diff --git a/.eslintrc.json b/.eslintrc.json index 916b556..98d4e69 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,6 +17,7 @@ }], "import/extensions": 0, "@typescript-eslint/no-explicit-any": 2, + "@typescript-eslint/no-use-before-define": 0, "no-underscore-dangle": 0, "no-console": 2, "no-param-reassign": 0, diff --git a/examples/example.html b/examples/example.html index 4a027a7..8cc334b 100644 --- a/examples/example.html +++ b/examples/example.html @@ -4,31 +4,42 @@ - -
-
- -
-

Artist:

-

Title:

-

-
+
+ +
+

Artist:

+

Title:

+

+ The Artist, Title and Image (above) are the actual client state of the currently playing track. Under normal circumstances, they should be 'prebufferLength' seconds behind the server state. The controls below represent the actual server state. +
-

Station controls should be private

-
@@ -62,7 +70,7 @@

Station controls should be private

return fetch(route) .then(res => res.json()) .then(x => { - console.log(x); + console.log(route, x); return x; }) } @@ -75,17 +83,16 @@

Station controls should be private

return urlCreator.createObjectURL(blob); } - function info() { - request('/info') + function getCurrentTrack() { + request('/getCurrentTrackInfo') .then(cur => { - document.getElementById('artist').innerHTML = `Artist: ${cur.artist}`; - document.getElementById('title').innerHTML = `Title: ${cur.title}`; + document.getElementById('artist-value').innerHTML = cur.artist; + document.getElementById('title-value').innerHTML = cur.title; document.getElementById('cover').src = getCoverUrl(cur.image) }) } - function controlsGetPlaylist() { - document.getElementById('getPlaylist').innerHTML = '' - request('/controls/getPlaylist') + function getPlaylist() { + request('/getPlaylist') .then((plist) => { const parent = document.getElementById('getPlaylist'); parent.innerHTML = ''; @@ -101,14 +108,24 @@

Station controls should be private

}) } function controlsShufflePlaylist() { - request('/controls/shufflePlaylist').then(controlsGetPlaylist) + request('/controls/shufflePlaylist').then(update) } function controlsNext() { - request('/controls/next').then(controlsGetPlaylist).then(info) + request('/controls/next').then(update) } function controlsRearrange({ oldIndex, newIndex }) { - request(`/controls/rearrangePlaylist?oldIndex=${oldIndex}&newIndex=${newIndex}`).then(controlsGetPlaylist) + request(`/controls/rearrangePlaylist?oldIndex=${oldIndex}&newIndex=${newIndex}`).then(getPlaylist) + } + + // short polling results and applying them after prebuffer length is applied + function update() { + return Promise.all([getCurrentTrack(), getPlaylist()]) } + setInterval(() => { + update() + }, 2000) + update() + Sortable.create(document.getElementById('getPlaylist'), { onEnd: controlsRearrange }); diff --git a/examples/server.ts b/examples/server.ts index 5519a87..e2a5a2a 100644 --- a/examples/server.ts +++ b/examples/server.ts @@ -1,16 +1,18 @@ /* eslint-disable no-console */ import path from 'path'; import express from 'express'; -import { SHUFFLE_METHODS, PUBLIC_EVENTS, Station } from '../src/index'; +import { SHUFFLE_METHODS, PUBLIC_EVENTS, Station, DEFAULTS } from '../src/index'; import type { ShallowTrackMeta } from '../src/index'; const port = 3001; const server = express(); const musicPath = path.resolve(process.cwd(), process.argv[2] || './examples/music'); +const prebufferLength = DEFAULTS.PREBUFFER_LENGTH; const station = new Station({ verbose: true, // for verbose logging to console responseHeaders: { 'icy-genre': 'jazz' }, + prebufferLength, }); // add folder to station station.addFolder(musicPath); @@ -19,16 +21,14 @@ station.addFolder(musicPath); let currentTrack: ShallowTrackMeta; station.on(PUBLIC_EVENTS.NEXT_TRACK, async (track) => { const result = await track.getMetaAsync(); - currentTrack = result; -}); - -station.on(PUBLIC_EVENTS.START, () => { - // double the playlist on start - station.reorderPlaylist((a) => a.concat(a)); -}); - -station.on(PUBLIC_EVENTS.RESTART, () => { - station.reorderPlaylist((a) => a.concat(a)); + if (!currentTrack) { + currentTrack = result; + } else { + // in order to compensate a lag between the server and client + setTimeout(() => { + currentTrack = result; + }, prebufferLength * 1000); + } }); // add this handler - otherwise any error will exit the process as unhandled @@ -41,10 +41,16 @@ server.get('/stream', (req, res) => { }); // get id3 tags of the track -server.get('/info', (_, res) => { +server.get('/getCurrentTrackInfo', (_, res) => { res.json(currentTrack); }); +// just get the entire playlist +server.get('/getPlaylist', (_, res) => { + const plist = station.getPlaylist(); + res.json(plist); +}); + // switch to the next track immediately server.get('/controls/next', (_, res) => { station.next(); @@ -64,12 +70,6 @@ server.get('/controls/rearrangePlaylist', (req, res) => { res.json(`Succesfully moved element from "${oldIndex}" to "${newIndex}"`); }); -// just get the entire playlist -server.get('/controls/getPlaylist', (_, res) => { - const plist = station.getPlaylist(); - res.json(plist); -}); - // route for serving static server.get('*', (_, res) => { res.sendFile(path.resolve(__dirname, './example.html')); diff --git a/package-lock.json b/package-lock.json index c847a60..8d1312b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@fridgefm/radio-core", - "version": "3.1.5", + "version": "3.2.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@fridgefm/radio-core", - "version": "3.1.5", + "version": "3.2.5", "license": "MIT", "dependencies": { + "@fridgefm/inverter": "^0.1.9", "chalk": "^4.1.2", "dev-null": "^0.1.1", "fs-extra": "^11.0.1", @@ -17,7 +18,7 @@ "klaw-sync": "^6.0.0", "node-id3": "^0.2.6", "typed-emitter": "^2.1.0", - "winston": "^3.3.3" + "winston": "^3.10.0" }, "devDependencies": { "@types/express": "^4.17.13", @@ -659,6 +660,14 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -1082,6 +1091,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fridgefm/inverter": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@fridgefm/inverter/-/inverter-0.1.16.tgz", + "integrity": "sha512-GR6s/nkXbDA7U4b9RWUIxm5eLcGF5MDUWV4OSfq6RjfVabQ+XI4T/FyDdgt0ST7cdPc5+7ndeQ1wcOwdYg49/w==", + "engines": { + "node": ">=14.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.6.0", "dev": true, @@ -1842,6 +1859,11 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/triple-beam": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz", + "integrity": "sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -2088,12 +2110,13 @@ } }, "node_modules/accepts": { - "version": "1.3.7", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, - "license": "MIT", "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" @@ -2380,8 +2403,9 @@ } }, "node_modules/async": { - "version": "3.2.1", - "license": "MIT" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -2512,37 +2536,43 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.19.0", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.0", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2622,9 +2652,10 @@ "license": "MIT" }, "node_modules/bytes": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2783,13 +2814,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/colors": { - "version": "1.4.0", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/colorspace": { "version": "1.1.4", "license": "MIT", @@ -2809,20 +2833,22 @@ "license": "MIT" }, "node_modules/content-disposition": { - "version": "0.5.3", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "license": "MIT", "dependencies": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" }, "engines": { "node": ">= 0.6" } }, "node_modules/content-type": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2834,9 +2860,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.0", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2846,10 +2873,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/create-require": { "version": "1.1.1", "dev": true, @@ -2935,11 +2958,12 @@ } }, "node_modules/depd": { - "version": "1.1.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/dequal": { @@ -2953,9 +2977,14 @@ } }, "node_modules/destroy": { - "version": "1.0.4", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, "node_modules/detect-newline": { "version": "3.1.0", @@ -3013,8 +3042,9 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/electron-to-chromium": { "version": "1.4.468", @@ -3045,8 +3075,9 @@ }, "node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3217,8 +3248,9 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -4023,8 +4055,9 @@ }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4079,37 +4112,39 @@ } }, "node_modules/express": { - "version": "4.17.1", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, - "license": "MIT", "dependencies": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -4184,8 +4219,9 @@ } }, "node_modules/fecha": { - "version": "4.2.1", - "license": "MIT" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -4210,16 +4246,17 @@ } }, "node_modules/finalhandler": { - "version": "1.1.2", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, "engines": { @@ -4228,16 +4265,18 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/find-up": { "version": "2.1.0", @@ -4290,8 +4329,9 @@ }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4642,25 +4682,21 @@ "dev": true }, "node_modules/http-errors": { - "version": "1.7.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, - "license": "MIT", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC" - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4672,8 +4708,9 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -5103,10 +5140,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "dev": true, @@ -6003,13 +6036,15 @@ "peer": true }, "node_modules/logform": { - "version": "2.3.0", - "license": "MIT", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", "dependencies": { - "colors": "^1.2.1", + "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", - "safe-stable-stringify": "^1.1.0", + "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, @@ -6077,8 +6112,9 @@ }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6124,8 +6160,9 @@ }, "node_modules/mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -6134,19 +6171,21 @@ } }, "node_modules/mime-db": { - "version": "1.50.0", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.33", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT", "dependencies": { - "mime-db": "1.50.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -6174,9 +6213,13 @@ } }, "node_modules/minimist": { - "version": "1.2.5", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/ms": { "version": "2.1.3", @@ -6188,9 +6231,10 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.2", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6354,9 +6398,10 @@ } }, "node_modules/on-finished": { - "version": "2.3.0", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -6490,8 +6535,9 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6623,10 +6669,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "license": "MIT" - }, "node_modules/progress": { "version": "2.0.3", "dev": true, @@ -6704,11 +6746,18 @@ ] }, "node_modules/qs": { - "version": "6.7.0", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, - "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/queue-microtask": { @@ -6732,19 +6781,21 @@ }, "node_modules/range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.4.0", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.2", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, @@ -6960,8 +7011,23 @@ "dev": true }, "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/safe-regex-test": { "version": "1.0.0", @@ -6978,8 +7044,12 @@ } }, "node_modules/safe-stable-stringify": { - "version": "1.1.1", - "license": "MIT" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -7001,23 +7071,24 @@ } }, "node_modules/send": { - "version": "0.17.1", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.7.2", + "http-errors": "2.0.0", "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", + "ms": "2.1.3", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" @@ -7025,55 +7096,39 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/http-errors": { - "version": "1.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/serve-static": { - "version": "1.14.1", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "dev": true, - "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.1" + "send": "0.18.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/setprototypeof": { - "version": "1.1.1", - "dev": true, - "license": "ISC" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shebang-command": { "version": "2.0.0", @@ -7204,11 +7259,12 @@ } }, "node_modules/statuses": { - "version": "1.5.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/string_decoder": { @@ -7218,24 +7274,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7479,16 +7517,21 @@ } }, "node_modules/toidentifier": { - "version": "1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/triple-beam": { - "version": "1.3.0", - "license": "MIT" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } }, "node_modules/ts-jest": { "version": "29.1.1", @@ -7602,9 +7645,10 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -7670,8 +7714,9 @@ }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -7790,8 +7835,9 @@ }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7955,58 +8001,44 @@ } }, "node_modules/winston": { - "version": "3.3.3", - "license": "MIT", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", "dependencies": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", + "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.2.0", + "logform": "^2.4.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" + "winston-transport": "^4.5.0" }, "engines": { - "node": ">= 6.4.0" + "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.4.0", - "license": "MIT", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", "dependencies": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" }, "engines": { "node": ">= 6.4.0" } }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "2.3.7", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/word-wrap": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8573,6 +8605,11 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -8789,6 +8826,11 @@ "strip-json-comments": "^3.1.1" } }, + "@fridgefm/inverter": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@fridgefm/inverter/-/inverter-0.1.16.tgz", + "integrity": "sha512-GR6s/nkXbDA7U4b9RWUIxm5eLcGF5MDUWV4OSfq6RjfVabQ+XI4T/FyDdgt0ST7cdPc5+7ndeQ1wcOwdYg49/w==" + }, "@humanwhocodes/config-array": { "version": "0.6.0", "dev": true, @@ -9428,6 +9470,11 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/triple-beam": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz", + "integrity": "sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g==" + }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -9563,11 +9610,13 @@ } }, "accepts": { - "version": "1.3.7", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" } }, "acorn": { @@ -9764,7 +9813,9 @@ "peer": true }, "async": { - "version": "3.2.1" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "available-typed-arrays": { "version": "1.0.5", @@ -9864,23 +9915,29 @@ "dev": true }, "body-parser": { - "version": "1.19.0", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, "requires": { - "bytes": "3.1.0", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "dependencies": { "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -9888,6 +9945,8 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -9940,7 +9999,9 @@ "dev": true }, "bytes": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, "call-bind": { @@ -10043,9 +10104,6 @@ "simple-swizzle": "^0.2.2" } }, - "colors": { - "version": "1.4.0" - }, "colorspace": { "version": "1.1.4", "requires": { @@ -10062,14 +10120,18 @@ "dev": true }, "content-disposition": { - "version": "0.5.3", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" } }, "content-type": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "convert-source-map": { @@ -10079,16 +10141,15 @@ "dev": true }, "cookie": { - "version": "0.4.0", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true }, "cookie-signature": { "version": "1.0.6", "dev": true }, - "core-util-is": { - "version": "1.0.3" - }, "create-require": { "version": "1.1.1", "dev": true, @@ -10151,7 +10212,9 @@ } }, "depd": { - "version": "1.1.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, "dequal": { @@ -10162,7 +10225,9 @@ "peer": true }, "destroy": { - "version": "1.0.4", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true }, "detect-newline": { @@ -10202,6 +10267,8 @@ }, "ee-first": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "electron-to-chromium": { @@ -10225,6 +10292,8 @@ }, "encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, "enquirer": { @@ -10365,6 +10434,8 @@ }, "escape-html": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, "escape-string-regexp": { @@ -10921,6 +10992,8 @@ }, "etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, "execa": { @@ -10961,36 +11034,39 @@ } }, "express": { - "version": "4.17.1", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "requires": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -11053,7 +11129,9 @@ } }, "fecha": { - "version": "4.2.1" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "file-entry-cache": { "version": "6.0.1", @@ -11070,20 +11148,24 @@ } }, "finalhandler": { - "version": "1.1.2", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, "dependencies": { "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -11091,6 +11173,8 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -11132,6 +11216,8 @@ }, "fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, "fs-extra": { @@ -11359,20 +11445,16 @@ "dev": true }, "http-errors": { - "version": "1.7.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "dev": true - } + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" } }, "human-signals": { @@ -11383,6 +11465,8 @@ }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -11653,9 +11737,6 @@ "call-bind": "^1.0.2" } }, - "isarray": { - "version": "1.0.0" - }, "isexe": { "version": "2.0.0", "dev": true @@ -12344,12 +12425,15 @@ "peer": true }, "logform": { - "version": "2.3.0", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", "requires": { - "colors": "^1.2.1", + "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", - "safe-stable-stringify": "^1.1.0", + "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, @@ -12402,6 +12486,8 @@ }, "media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, "merge-descriptors": { @@ -12432,17 +12518,23 @@ }, "mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, "mime-db": { - "version": "1.50.0", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.33", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.50.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -12461,7 +12553,9 @@ } }, "minimist": { - "version": "1.2.5", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "ms": { @@ -12472,7 +12566,9 @@ "dev": true }, "negotiator": { - "version": "0.6.2", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true }, "node-id3": { @@ -12591,7 +12687,9 @@ } }, "on-finished": { - "version": "2.3.0", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "requires": { "ee-first": "1.1.1" @@ -12683,6 +12781,8 @@ }, "parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true }, "path-exists": { @@ -12762,9 +12862,6 @@ } } }, - "process-nextick-args": { - "version": "2.0.1" - }, "progress": { "version": "2.0.3", "dev": true @@ -12819,8 +12916,13 @@ "dev": true }, "qs": { - "version": "6.7.0", - "dev": true + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } }, "queue-microtask": { "version": "1.2.3", @@ -12828,14 +12930,18 @@ }, "range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, "raw-body": { - "version": "2.4.0", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "dev": true, "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.2", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } @@ -12980,7 +13086,9 @@ } }, "safe-buffer": { - "version": "5.1.2" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex-test": { "version": "1.0.0", @@ -12994,7 +13102,9 @@ } }, "safe-stable-stringify": { - "version": "1.1.1" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" }, "safer-buffer": { "version": "2.1.2" @@ -13009,26 +13119,30 @@ } }, "send": { - "version": "0.17.1", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dev": true, "requires": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.7.2", + "http-errors": "2.0.0", "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", + "ms": "2.1.3", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "dependencies": { "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -13036,39 +13150,30 @@ "dependencies": { "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } - }, - "http-errors": { - "version": "1.7.3", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "ms": { - "version": "2.1.1", - "dev": true } } }, "serve-static": { - "version": "1.14.1", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "dev": true, "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.1" + "send": "0.18.0" } }, "setprototypeof": { - "version": "1.1.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, "shebang-command": { @@ -13164,18 +13269,15 @@ } }, "statuses": { - "version": "1.5.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, "string_decoder": { "version": "1.3.0", "requires": { "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1" - } } }, "string-length": { @@ -13353,11 +13455,15 @@ } }, "toidentifier": { - "version": "1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, "triple-beam": { - "version": "1.3.0" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" }, "ts-jest": { "version": "29.1.1", @@ -13415,7 +13521,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -13458,6 +13566,8 @@ }, "type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "requires": { "media-typer": "0.3.0", @@ -13542,6 +13652,8 @@ }, "unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, "update-browserslist-db": { @@ -13654,48 +13766,37 @@ } }, "winston": { - "version": "3.3.3", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", "requires": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", + "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.2.0", + "logform": "^2.4.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" + "winston-transport": "^4.5.0" } }, "winston-transport": { - "version": "4.4.0", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" } }, "word-wrap": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 20e17e4..547ad31 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@fridgefm/radio-core", "author": "Grigory Gorshkov", - "version": "3.1.5", + "version": "3.2.5", "description": "internet radio engine made on NodeJS platform", "license": "MIT", "main": "./lib/index.js", @@ -28,6 +28,7 @@ "test:unit:cov": "jest --config jest.config.json --collectCoverage=true" }, "dependencies": { + "@fridgefm/inverter": "^0.1.9", "chalk": "^4.1.2", "dev-null": "^0.1.1", "fs-extra": "^11.0.1", @@ -36,7 +37,7 @@ "klaw-sync": "^6.0.0", "node-id3": "^0.2.6", "typed-emitter": "^2.1.0", - "winston": "^3.3.3" + "winston": "^3.10.0" }, "devDependencies": { "@types/express": "^4.17.13", diff --git a/src/__tests__/fail-path/common.fp.test.ts b/src/__tests__/fail-path/common.fp.spec.ts similarity index 100% rename from src/__tests__/fail-path/common.fp.test.ts rename to src/__tests__/fail-path/common.fp.spec.ts diff --git a/src/__tests__/happy-path/event.hp.test.ts b/src/__tests__/happy-path/event.hp.spec.ts similarity index 100% rename from src/__tests__/happy-path/event.hp.test.ts rename to src/__tests__/happy-path/event.hp.spec.ts diff --git a/src/__tests__/happy-path/station.hp.test.ts b/src/__tests__/happy-path/station.hp.spec.ts similarity index 100% rename from src/__tests__/happy-path/station.hp.test.ts rename to src/__tests__/happy-path/station.hp.spec.ts diff --git a/src/__tests__/test-utils.mock.ts b/src/__tests__/test-utils.mock.ts index 9a83087..10f3ed8 100644 --- a/src/__tests__/test-utils.mock.ts +++ b/src/__tests__/test-utils.mock.ts @@ -29,12 +29,3 @@ export class TestFile { fs.removeSync(this.fullPath); } } - -// describe('test-utils', () => { -// it('new TestFile', () => { -// const t1 = new TestFile(); -// expect(fs.statSync(t1.fullPath).isFile()).toEqual(true); -// t1.remove(); -// expect(() => fs.statSync(t1.fullPath).isFile()).toThrow(); -// }); -// }); diff --git a/src/base/Playlist/Playlist.ts b/src/base/Playlist/Playlist.ts deleted file mode 100644 index d0893c4..0000000 --- a/src/base/Playlist/Playlist.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createList } from '../../utils/fs'; -import { captureTime } from '../../utils/time'; -import { PUBLIC_EVENTS } from '../../features/EventBus/events'; -import { createTrackMap } from './methods'; - -import type { TPlaylist, TrackMap, ReorderCb, PathList } from './Playlist.types'; -import type { EventBus } from '../../features/EventBus/EventBus'; -import type { InfoEvent } from '../../features/EventBus/events'; -import type { TTrack } from '../Track/Track.types'; - -type Deps = { eventBus: EventBus }; - -export const createPlaylist = (deps: Deps) => { - const folders = new Set(); - const emitInfo = (a: InfoEvent) => deps.eventBus.emit(PUBLIC_EVENTS.INFO, { name: 'playlist', ...a }); - - let currentIndex = -1; - let list: PathList = []; - let tracksMap: TrackMap = new Map(); - - const instance: TPlaylist = { - addFolder: (folder: string) => { - folders.add(folder); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return revalidate(); - }, - getList: () => { - return list.map((v, i) => { - const tra = tracksMap.get(v) as TTrack; - - return { - ...tra, - isPlaying: currentIndex === i, - }; - }); - }, - reorder: (cb: ReorderCb) => { - const ct = captureTime(); - const prevList = instance.getList(); - const currentlyPlaying = prevList.find((v) => !!v.isPlaying); - - list = cb(prevList).map((b) => b.fsStats.fullPath); - currentIndex = list.findIndex((v) => v === currentlyPlaying?.fsStats.fullPath); - - emitInfo({ - level: 'info', - event: 'reorder', - message: 'Playlist reordered', - timings: ct(), - }); - - return instance.getList(); - }, - getNext: () => { - if (list.length - 1 === currentIndex) { - // the playlist drained - const ct = captureTime(); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - revalidate(); - currentIndex = 0; - deps.eventBus.emit(PUBLIC_EVENTS.RESTART, instance.getList(), ct()); - } else { - currentIndex += 1; - } - const nextPath = list[currentIndex] as string; - const nextTrack = tracksMap.get(nextPath); - - if (!nextTrack) { - emitInfo({ level: 'warn', event: 'no-next-track', message: `No next track found for ${nextPath}` }); - // try next tracks - return instance.getNext(); - } - nextTrack.playCount += 1; - - return { ...nextTrack, isPlaying: true }; - }, - }; - - const revalidate = () => { - const ct = captureTime(); - list = createList(Array.from(folders)); - tracksMap = createTrackMap(list); - - const result = instance.getList(); - emitInfo({ event: 'revalidate', message: 'Playlist revalidated', timings: ct() }); - return result; - }; - - return instance; -}; diff --git a/src/base/Playlist/__tests__/methods.test.ts b/src/base/Playlist/__tests__/methods.test.ts deleted file mode 100644 index 420d14b..0000000 --- a/src/base/Playlist/__tests__/methods.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createTrackMap } from '../methods'; -import { tracks } from '../../../__tests__/test-utils.mock'; - -const tracksArr = tracks.map((v) => v.fullPath); - -describe('base/Playlist/createTrackMap', () => { - it('dedupes by path', () => { - const map = createTrackMap([...tracksArr, ...tracksArr]); - expect(Array.from(map)).toHaveLength(2); - }); - - it('inits with zero values', () => { - const map = createTrackMap(tracksArr); - expect(Array.from(map).every(([, tr]) => tr.playCount === 0)).toEqual(true); - }); -}); diff --git a/src/base/Playlist/methods.ts b/src/base/Playlist/methods.ts deleted file mode 100644 index cb424dd..0000000 --- a/src/base/Playlist/methods.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Mp3 from '../../utils/mp3'; -import { extractLast, shuffleArray } from '../../utils/funcs'; -import { createTrack } from '../Track/Track'; - -import type { TrackList, TrackMap } from './Playlist.types'; - -export const createTrackMap = (paths: readonly string[]): TrackMap => - paths - .filter((path) => { - const f = extractLast(path, '/'); - - return Mp3.isSupported(f[1]); - }) - .reduce((acc, path) => { - // deduplicate if already in map - if (acc.has(path)) { - return acc; - } - - return acc.set(path, createTrack(path)); - }, new Map() as TrackMap); - -export const SHUFFLE_METHODS = { - rearrange: - ({ to, from }: { to: number; from: number }) => - (arr: TrackList) => { - const movedElement = arr.splice(from, 1)[0]; - if (movedElement) { - arr.splice(to, 0, movedElement); - } - - return arr; - }, - randomShuffle: () => shuffleArray, -}; diff --git a/src/base/Queuestream.ts b/src/base/Queuestream.ts deleted file mode 100644 index f9c39cf..0000000 --- a/src/base/Queuestream.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Readable, Transform, Writable } from 'stream'; -import devnull from 'dev-null'; -import { captureTime } from '../utils/time'; -import { createPrebuffer } from '../features/Prebuffer'; -import { PUBLIC_EVENTS } from '../features/EventBus/events'; - -import type { EventBus } from '../features/EventBus/EventBus'; -import type { TPlaylist } from './Playlist/Playlist.types'; - -type Deps = { - playlist: TPlaylist; - eventBus: EventBus; -}; - -export class QueueStream { - private _deps: Deps; - - // this stream is always live - private _current = new Transform({ - transform: (chunk, _, callback) => { - this._prebuffer.modify([chunk]); - callback(undefined, chunk); - }, - }); - - // this stream switches on each track - private _trackStream: Readable; - - // prebuffering for faster client response (side-effect) - private _prebuffer = createPrebuffer(); - - constructor(deps: Deps) { - this._deps = deps; - this.currentPipe(devnull(), { end: false }); - this._trackStream = new Readable(); - } - - private _handleError(error: Error, event: string) { - this._deps.eventBus.emit(PUBLIC_EVENTS.ERROR, { - name: 'queuestream', - error, - event, - }); - this.next(); - } - - public getPrebuffer = () => this._prebuffer.getStorage(); - - public currentPipe = (wrstr: Writable, opts = {}) => this._current.pipe(wrstr, opts); - - public next = () => { - const ct = captureTime(); - const { playlist, eventBus } = this._deps; - const nextTrack = playlist.getNext(); - - // destroy previous track stream if there was one - this._trackStream?.destroy(); - - // populate newly created stream with some handlers - const [error, newStream] = nextTrack.getSound(); - if (error) { - this._handleError(error, 'get-sound-error'); - return; - } - - newStream.once('error', (e) => this._handleError(e, 'stream-error')); - newStream.once('end', this.next); - newStream.pipe(this._current, { end: false }); - - this._trackStream = newStream; - - eventBus.emit(PUBLIC_EVENTS.NEXT_TRACK, nextTrack, ct()); - }; -} diff --git a/src/base/Station.ts b/src/base/Station.ts deleted file mode 100644 index ab60046..0000000 --- a/src/base/Station.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createEventBus, EventBus } from '../features/EventBus/EventBus'; -import { PUBLIC_EVENTS } from '../features/EventBus/events'; -import { captureTime } from '../utils/time'; -import { mergeConfig, Config } from '../config/index'; -import { createPlaylist } from './Playlist/Playlist'; -import { QueueStream } from './Queuestream'; - -import type { ClientRequest, ServerResponse } from 'http'; -import type { TStation } from '../types/public'; -import type { TEmitter } from '../features/EventBus/events'; -import type { TPlaylist, ReorderCb } from './Playlist/Playlist.types'; - -type StationDeps = { - queuestream: QueueStream; - eventBus: EventBus; - playlist: TPlaylist; - config: Config; -}; - -export class Station implements TStation { - private _deps: StationDeps; - - constructor(extConfig?: Partial) { - const config = mergeConfig(extConfig || {}); - const eventBus = createEventBus({ config }); - const playlist = createPlaylist({ eventBus }); - const queuestream = new QueueStream({ playlist, eventBus }); - - this._deps = { - playlist, - eventBus, - queuestream, - config, - }; - } - - public start() { - const ct = captureTime(); - const { eventBus, queuestream } = this._deps; - - if (this.getPlaylist().length) { - queuestream.next(); - } - - eventBus.emit(PUBLIC_EVENTS.START, this.getPlaylist(), ct()); - } - - public togglePause(): void {} - - public addFolder(folder: string) { - return this._deps.playlist.addFolder(folder); - } - - public next() { - return this._deps.queuestream.next(); - } - - public getPlaylist() { - return this._deps.playlist.getList(); - } - - public connectListener(_: ClientRequest, res: ServerResponse, cb = () => {}) { - const { currentPipe, getPrebuffer } = this._deps.queuestream; - - res.writeHead(200, this._deps.config.responseHeaders); - res.write(getPrebuffer()); - currentPipe(res); - cb(); - } - - public reorderPlaylist(cb: ReorderCb) { - return this._deps.playlist.reorder(cb); - } - - public on: TEmitter['on'] = (...args) => this._deps.eventBus.on(...args); -} diff --git a/src/base/Track/Track.ts b/src/base/Track/Track.ts deleted file mode 100644 index 669fe81..0000000 --- a/src/base/Track/Track.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSoundStream, getMetaAsync, getStats } from './methods'; -import type { TTrack } from './Track.types'; - -export const createTrack = (fullPath: string): TTrack => { - const fsStats = getStats(fullPath); - - return { - fsStats, - playCount: 0, - getMetaAsync: () => getMetaAsync(fsStats), - getSound: () => createSoundStream(fsStats), - }; -}; diff --git a/src/base/__tests__/Queuestream.test.ts b/src/base/__tests__/Queuestream.test.ts deleted file mode 100644 index e7d07ce..0000000 --- a/src/base/__tests__/Queuestream.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('base/Queuestream', () => { - it.todo('test all methods'); -}); diff --git a/src/config/index.ts b/src/config/index.ts deleted file mode 100644 index f60c704..0000000 --- a/src/config/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { responseHeaders } from './responseHeaders'; - -export const defaultConfig = { - /** - * Pass your custom response headers from the station endpoint - */ - responseHeaders: responseHeaders(), - /** - * If set to true enables verbose info logging - */ - verbose: false, -}; - -export const mergeConfig = (cfg: Partial) => - ({ - ...cfg, - responseHeaders: responseHeaders(cfg.responseHeaders), - } as Config); - -export type Config = typeof defaultConfig; diff --git a/src/config/responseHeaders.ts b/src/config/responseHeaders.ts deleted file mode 100644 index 1c801bf..0000000 --- a/src/config/responseHeaders.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const responseHeaders = (cfg?: Record) => - ({ - ...(cfg || { - 'icy-br': '56', - 'icy-genre': 'house', - 'icy-metaint': '0', - 'icy-pub': '0', - 'icy-url': 'https://', - }), - 'icy-name': '@fridgefm/radio-engine', - 'icy-notice1': 'Live radio powered by https://www.npmjs.com/package/@fridgefm/radio-engine', - 'Cache-Control': 'no-cache,no-store,must-revalidate,max-age=0', - 'Content-Type': 'audio/mpeg', - } as Record); diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index cfa514f..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DEFAULTS = { - PREBUFFER_LENGTH: 12, -}; diff --git a/src/features/EventBus/EventBus.ts b/src/features/EventBus/EventBus.ts deleted file mode 100644 index a35a22d..0000000 --- a/src/features/EventBus/EventBus.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { EventEmitter } from 'events'; -import { logger } from '../../utils/logger'; -import { defaultConfig } from '../../config/index'; -import { PUBLIC_EVENTS } from './events'; - -import type { TEmitter, InfoEvent, ErrorEvent } from './events'; - -/** - * This entity enables event system, its emitting and listenings. - */ -export const createEventBus = ({ config } = { config: defaultConfig }) => { - const emitter = new EventEmitter() as TEmitter; - const on: TEmitter['on'] = (eventName, handler) => emitter.on(eventName, handler); - const emit: TEmitter['emit'] = (eventName, ...args) => emitter.emit(eventName, ...args); - - on(PUBLIC_EVENTS.NEXT_TRACK, (tr, timings) => - emit(PUBLIC_EVENTS.INFO, { event: PUBLIC_EVENTS.NEXT_TRACK, message: tr.fsStats.stringified, timings }), - ); - on(PUBLIC_EVENTS.START, (_, timings) => - emit(PUBLIC_EVENTS.INFO, { event: PUBLIC_EVENTS.START, message: 'Station started', timings }), - ); - on(PUBLIC_EVENTS.RESTART, (_, timings) => - emit(PUBLIC_EVENTS.INFO, { event: PUBLIC_EVENTS.RESTART, message: 'Station restarted', timings }), - ); - - if (config.verbose) { - on(PUBLIC_EVENTS.INFO, ({ level, ...rest }: InfoEvent) => logger.log(level || 'info', { ...rest })); - on(PUBLIC_EVENTS.ERROR, (event: ErrorEvent) => logger.log('error', { ...event, message: event.error.message })); - } - - return { on, emit }; -}; - -export type EventBus = ReturnType; diff --git a/src/features/Prebuffer.ts b/src/features/Prebuffer.ts deleted file mode 100644 index c003adc..0000000 --- a/src/features/Prebuffer.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Buffer } from 'buffer'; -import { DEFAULTS } from '../constants'; - -type PrebufferArgs = { prebufferLength?: number }; - -/** - * Helper object that stores previous [prebufferLength] of Buffers. - * It enables prebuffering, simply put it just immediately returns [prebufferLength] previous seconds of the stream. - */ -export const createPrebuffer = (args: PrebufferArgs = {}) => { - const prebufferLength = args.prebufferLength || DEFAULTS.PREBUFFER_LENGTH; - const storage: Buffer[] = []; - - return { - modify: (chunks: Buffer[]) => { - chunks.forEach((ch) => { - if (storage.length >= prebufferLength) { - storage.shift(); - } - - storage.push(ch); - }); - }, - getStorage: () => { - const totalPrebufferLength = (storage[0] || []).length * prebufferLength; - - return Buffer.concat(storage, totalPrebufferLength); - }, - }; -}; diff --git a/src/index.ts b/src/index.ts index d7133b7..ab1a659 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,6 @@ -import { Station } from './base/Station'; -import { PUBLIC_EVENTS } from './features/EventBus/events'; -import { SHUFFLE_METHODS } from './base/Playlist/methods'; -import { DEFAULTS } from './constants'; +export { Station } from './providers/station'; +export { PUBLIC_EVENTS } from './providers/events/events.provider'; +export { SHUFFLE_METHODS } from './providers/playlist/playlist.methods'; +export { DEFAULTS } from './providers/config'; -export { Station, PUBLIC_EVENTS, SHUFFLE_METHODS, DEFAULTS }; -export type { ShallowTrackMeta, TTrack, TrackStats } from './base/Track/Track.types'; -export type { ReorderCb, TrackList, TPlaylist } from './base/Playlist/Playlist.types'; -export type { TStation } from './types/public'; +export type * from './types/public.types'; diff --git a/src/providers/config.ts b/src/providers/config.ts new file mode 100644 index 0000000..445f6a7 --- /dev/null +++ b/src/providers/config.ts @@ -0,0 +1,30 @@ +export type Config = { + responseHeaders: Record; + verbose: boolean; + prebufferLength: number; +}; + +const responseHeaders = (cfg?: Record) => ({ + ...(cfg || { + 'icy-br': '56', + 'icy-genre': 'house', + 'icy-metaint': '0', + 'icy-pub': '0', + 'icy-url': 'https://', + }), + 'icy-name': '@fridgefm/radio-core', + 'icy-notice1': 'Live radio powered by https://www.npmjs.com/package/@fridgefm/radio-core', + 'Cache-Control': 'no-cache,no-store,must-revalidate,max-age=0', + 'Content-Type': 'audio/mpeg', +}); + +export const createConfig = (cfg: Partial): Config => ({ + verbose: false, + prebufferLength: 12, + ...cfg, + responseHeaders: responseHeaders(cfg.responseHeaders), +}); + +export const DEFAULTS = { + PREBUFFER_LENGTH: 12, +}; diff --git a/src/features/__tests__/EventBus.test.ts b/src/providers/events/__tests__/events.spec.ts similarity index 88% rename from src/features/__tests__/EventBus.test.ts rename to src/providers/events/__tests__/events.spec.ts index 96efeea..88e8b32 100644 --- a/src/features/__tests__/EventBus.test.ts +++ b/src/providers/events/__tests__/events.spec.ts @@ -1,5 +1,4 @@ -import { PUBLIC_EVENTS } from '../EventBus/events'; -import { createEventBus } from '../EventBus/EventBus'; +import { eventBusFactory, PUBLIC_EVENTS } from '../events.provider'; const createMocks = () => ({ start: jest.fn(), @@ -15,7 +14,7 @@ describe('features/EventBus', () => { ['NEXT_TRACK', { message: 'stringified-mock', payload: { fsStats: { stringified: 'stringified-mock' } } }], ])('"%s" infos public event', (eventName, { message, payload }) => { const mocks = createMocks(); - const instance = createEventBus(); + const instance = eventBusFactory({}); const lowerCased = eventName.toLowerCase(); instance.on(PUBLIC_EVENTS.INFO, mocks.info); diff --git a/src/providers/events/events.provider.ts b/src/providers/events/events.provider.ts new file mode 100644 index 0000000..21f3817 --- /dev/null +++ b/src/providers/events/events.provider.ts @@ -0,0 +1,65 @@ +import { EventEmitter } from 'events'; +import { injectable, createToken } from '@fridgefm/inverter'; +import { logger } from '../../utils/logger'; +import { CONFIG_TOKEN } from '../tokens'; + +import type { ErrorEvent, InfoEvent, TEmitter } from './events.types'; +import type { Config } from '../config'; + +export const EVENT_BUS_TOKEN = createToken>('event_bus'); + +export const PUBLIC_EVENTS = { + ERROR: 'error', + INFO: 'einfo', + START: 'estart', + RESTART: 'erestart', + NEXT_TRACK: 'enexttrack', +} as const; + +export const eventBusFactory = (config: Config) => { + const emitter = new EventEmitter() as TEmitter; + const publicEventBus: Pick = { + on: (eventName, handler) => emitter.on(eventName, handler), + emit: (eventName, ...args) => emitter.emit(eventName, ...args), + }; + + if (config.verbose) { + publicEventBus.on(PUBLIC_EVENTS.INFO, ({ level, ...rest }: InfoEvent) => logger.log(level || 'info', { ...rest })); + + publicEventBus.on(PUBLIC_EVENTS.ERROR, (event: ErrorEvent) => + logger.log('error', { ...event, message: event.error.message }), + ); + } + + publicEventBus.on(PUBLIC_EVENTS.NEXT_TRACK, (tr, timings) => + publicEventBus.emit(PUBLIC_EVENTS.INFO, { + event: PUBLIC_EVENTS.NEXT_TRACK, + message: tr.fsStats.stringified, + timings, + }), + ); + + publicEventBus.on(PUBLIC_EVENTS.START, (_, timings) => + publicEventBus.emit(PUBLIC_EVENTS.INFO, { + event: PUBLIC_EVENTS.START, + message: 'Station started', + timings, + }), + ); + + publicEventBus.on(PUBLIC_EVENTS.RESTART, (_, timings) => + publicEventBus.emit(PUBLIC_EVENTS.INFO, { + event: PUBLIC_EVENTS.RESTART, + message: 'Station restarted', + timings, + }), + ); + + return publicEventBus; +}; + +export const eventBusProvider = injectable({ + provide: EVENT_BUS_TOKEN, + useFactory: eventBusFactory, + inject: [CONFIG_TOKEN] as const, +}); diff --git a/src/features/EventBus/events.ts b/src/providers/events/events.types.ts similarity index 70% rename from src/features/EventBus/events.ts rename to src/providers/events/events.types.ts index 6dcf01f..ad7c217 100644 --- a/src/features/EventBus/events.ts +++ b/src/providers/events/events.types.ts @@ -1,14 +1,7 @@ import type TypedEmitter from 'typed-emitter'; -import type { TrackList } from '../../base/Playlist/Playlist.types'; -import type { TTrack } from '../../base/Track/Track.types'; - -export const PUBLIC_EVENTS = { - ERROR: 'error', - INFO: 'einfo', - START: 'estart', - RESTART: 'erestart', - NEXT_TRACK: 'enexttrack', -} as const; +import type { PUBLIC_EVENTS } from './events.provider'; +import type { TrackList } from '../playlist/playlist.types'; +import type { TTrack } from '../track/track.types'; type BaseEvent = { event: string; diff --git a/src/base/Playlist/__tests__/Playlist.test.ts b/src/providers/playlist/__tests__/playlist.spec.ts similarity index 100% rename from src/base/Playlist/__tests__/Playlist.test.ts rename to src/providers/playlist/__tests__/playlist.spec.ts diff --git a/src/providers/playlist/playlist.methods.ts b/src/providers/playlist/playlist.methods.ts new file mode 100644 index 0000000..8c02a09 --- /dev/null +++ b/src/providers/playlist/playlist.methods.ts @@ -0,0 +1,16 @@ +import { shuffleArray } from '../../utils/funcs'; +import type { TrackList } from './playlist.types'; + +export const SHUFFLE_METHODS = { + rearrange: + ({ to, from }: { to: number; from: number }) => + (arr: TrackList) => { + const movedElement = arr.splice(from, 1)[0]; + if (movedElement) { + arr.splice(to, 0, movedElement); + } + + return arr; + }, + randomShuffle: () => shuffleArray, +}; diff --git a/src/providers/playlist/playlist.provider.ts b/src/providers/playlist/playlist.provider.ts new file mode 100644 index 0000000..7a3214b --- /dev/null +++ b/src/providers/playlist/playlist.provider.ts @@ -0,0 +1,109 @@ +import { injectable, createToken } from '@fridgefm/inverter'; +import { createList } from '../../utils/fs'; +import { captureTime } from '../../utils/time'; +import { PUBLIC_EVENTS, EVENT_BUS_TOKEN } from '../events/events.provider'; +import { TRACK_FACTORY_TOKEN } from '../track/track.provider'; +import { extractLast } from '../../utils/funcs'; +import Mp3 from '../../utils/mp3'; + +import type { TTrack } from '../track/track.types'; +import type { InfoEvent } from '../events/events.types'; +import type { TrackMap, TrackList, ReorderCb, PathList, PlaylistElement, TPlaylist } from './playlist.types'; + +export const PLAYLIST_TOKEN = createToken('playlist'); + +export const playlistProvider = injectable({ + provide: PLAYLIST_TOKEN, + scope: 'singleton', + useFactory: (createTrack, eventBus) => { + const folders: Set = new Set(); + let currentIndex = -1; + let tracksMap: TrackMap = new Map(); + let list: PathList = []; + + const emitInfo = (a: InfoEvent) => { + eventBus.emit(PUBLIC_EVENTS.INFO, { name: 'playlist', ...a }); + }; + + const revalidate = () => { + const ct = captureTime(); + list = createList(Array.from(folders)); + tracksMap = list + .filter((path) => { + const f = extractLast(path, '/'); + + return Mp3.isSupported(f[1]); + }) + .reduce((acc, path) => { + // deduplicate if already in map + if (acc.has(path)) { + return acc; + } + + return acc.set(path, createTrack(path)); + }, new Map() as TrackMap); + + const result = publicPlaylist.getList(); + emitInfo({ event: 'revalidate', message: 'Playlist revalidated', timings: ct() }); + return result; + }; + + const publicPlaylist = { + getList: (): TrackList => + list.map((v, i) => { + const tra = tracksMap.get(v) as TTrack; + + return { + ...tra, + isPlaying: currentIndex === i, + }; + }), + getNext: (): PlaylistElement => { + if (list.length - 1 === currentIndex) { + // the playlist drained + const ct = captureTime(); + revalidate(); + currentIndex = 0; + eventBus.emit(PUBLIC_EVENTS.RESTART, publicPlaylist.getList(), ct()); + } else { + currentIndex += 1; + } + const nextPath = list[currentIndex] as string; + const nextTrack = tracksMap.get(nextPath); + + if (!nextTrack) { + emitInfo({ level: 'warn', event: 'no-next-track', message: `No next track found for ${nextPath}` }); + // try next tracks + return publicPlaylist.getNext(); + } + nextTrack.playCount += 1; + + return { ...nextTrack, isPlaying: true }; + }, + addFolder: (folder: string) => { + folders.add(folder); + return revalidate(); + }, + reorder: (cb: ReorderCb) => { + const ct = captureTime(); + const prevList = publicPlaylist.getList(); + const currentlyPlaying = prevList.find((v) => !!v.isPlaying); + + list = cb(prevList).map((b) => b.fsStats.fullPath); + currentIndex = list.findIndex((v) => v === currentlyPlaying?.fsStats.fullPath); + + emitInfo({ + level: 'info', + event: 'reorder', + message: 'Playlist reordered', + timings: ct(), + }); + + return publicPlaylist.getList(); + }, + }; + + return publicPlaylist; + }, + inject: [TRACK_FACTORY_TOKEN, EVENT_BUS_TOKEN] as const, +}); diff --git a/src/base/Playlist/Playlist.types.ts b/src/providers/playlist/playlist.types.ts similarity index 87% rename from src/base/Playlist/Playlist.types.ts rename to src/providers/playlist/playlist.types.ts index 8651c33..afc3aad 100644 --- a/src/base/Playlist/Playlist.types.ts +++ b/src/providers/playlist/playlist.types.ts @@ -1,4 +1,4 @@ -import type { TTrack, TrackPath } from '../Track/Track.types'; +import type { TTrack, TrackPath } from '../track/track.types'; export type PlaylistElement = { isPlaying: boolean; diff --git a/src/features/__tests__/Prebuffer.test.ts b/src/providers/prebuffer/__tests__/prebuffer.spec.ts similarity index 71% rename from src/features/__tests__/Prebuffer.test.ts rename to src/providers/prebuffer/__tests__/prebuffer.spec.ts index 53abd81..2138971 100644 --- a/src/features/__tests__/Prebuffer.test.ts +++ b/src/providers/prebuffer/__tests__/prebuffer.spec.ts @@ -1,11 +1,11 @@ import { Buffer } from 'buffer'; -import { createPrebuffer } from '../Prebuffer'; +import { prebufferFactory } from '../prebuffer.provider'; const createChunks = (from: string) => from.split('').map((v) => Buffer.from([Number(v)])); describe('features/Prebuffer', () => { it('adds chunks up to max', () => { - const instance = createPrebuffer({ prebufferLength: 10 }); + const instance = prebufferFactory({ prebufferLength: 10 }); expect(instance.getStorage()).toHaveLength(0); instance.modify(createChunks('1')); @@ -15,7 +15,7 @@ describe('features/Prebuffer', () => { }); it('does not overflow max values', () => { - const instance = createPrebuffer({ prebufferLength: 10 }); + const instance = prebufferFactory({ prebufferLength: 10 }); instance.modify(createChunks('1234567890')); expect(instance.getStorage()).toHaveLength(10); @@ -28,11 +28,4 @@ describe('features/Prebuffer', () => { instance.modify(createChunks('1234')); expect(instance.getStorage().join('')).toEqual('9012341234'); }); - - it('uses default if length not set', () => { - const instance = createPrebuffer(); - instance.modify(createChunks('1')); - - expect(instance.getStorage()).toHaveLength(12); - }); }); diff --git a/src/providers/prebuffer/prebuffer.provider.ts b/src/providers/prebuffer/prebuffer.provider.ts new file mode 100644 index 0000000..037c9e7 --- /dev/null +++ b/src/providers/prebuffer/prebuffer.provider.ts @@ -0,0 +1,38 @@ +import { injectable, createToken } from '@fridgefm/inverter'; +import { CONFIG_TOKEN } from '../tokens'; + +type PrebufferArgs = { prebufferLength: number }; +type PrebufferT = { + getStorage: () => Buffer; + modify: (chunks: Buffer[]) => void; +}; + +export const PREBUFFER_TOKEN = createToken('prebuffer'); + +export const prebufferFactory = (config: PrebufferArgs) => { + const { prebufferLength } = config; + const storage: Buffer[] = []; + + return { + modify: (chunks: Buffer[]) => { + chunks.forEach((ch) => { + if (storage.length >= prebufferLength) { + storage.shift(); + } + + storage.push(ch); + }); + }, + getStorage: () => { + const totalPrebufferLength = (storage[0] || []).length * prebufferLength; + return Buffer.concat(storage, totalPrebufferLength); + }, + }; +}; + +export const prebufferProvider = injectable({ + provide: PREBUFFER_TOKEN, + scope: 'scoped', + useFactory: prebufferFactory, + inject: [CONFIG_TOKEN] as const, +}); diff --git a/src/providers/queuestream/queuestream.provider.ts b/src/providers/queuestream/queuestream.provider.ts new file mode 100644 index 0000000..b26708a --- /dev/null +++ b/src/providers/queuestream/queuestream.provider.ts @@ -0,0 +1,65 @@ +import { Readable, Transform, Writable } from 'stream'; +import { injectable, createToken } from '@fridgefm/inverter'; +import devnull from 'dev-null'; +import { captureTime } from '../../utils/time'; +import { PUBLIC_EVENTS, EVENT_BUS_TOKEN } from '../events/events.provider'; +import { PREBUFFER_TOKEN } from '../prebuffer/prebuffer.provider'; +import { PLAYLIST_TOKEN } from '../playlist/playlist.provider'; +import type { Queuestream } from './queuestream.types'; + +export const QUEUESTREAM_TOKEN = createToken('queuestream'); + +export const queuestreamProvider = injectable({ + provide: QUEUESTREAM_TOKEN, + scope: 'singleton', + useFactory: (eventBus, playlist, prebuffer) => { + let trackStream = new Readable(); + const currentStream = new Transform({ + transform: (chunk, _, callback) => { + prebuffer.modify([chunk]); + callback(undefined, chunk); + }, + }); + + const handleError = (error: Error, event: string) => { + eventBus.emit(PUBLIC_EVENTS.ERROR, { + name: 'queuestream', + error, + event, + }); + publicQueuestream.next(); + }; + + const publicQueuestream = { + getPrebuffer: () => prebuffer.getStorage(), + currentPipe: (wrstr: Writable, opts?: { end?: boolean }) => currentStream.pipe(wrstr, opts), + next: () => { + const ct = captureTime(); + const nextTrack = playlist.getNext(); + + // destroy previous track stream if there was one + trackStream?.destroy(); + + // populate newly created stream with some handlers + const [error, newStream] = nextTrack.getSound(); + if (error) { + handleError(error, 'get-sound-error'); + return; + } + + newStream.once('error', (e) => handleError(e, 'stream-error')); + newStream.once('end', publicQueuestream.next); + newStream.pipe(currentStream, { end: false }); + + trackStream = newStream; + + eventBus.emit(PUBLIC_EVENTS.NEXT_TRACK, nextTrack, ct()); + }, + }; + + publicQueuestream.currentPipe(devnull(), { end: false }); + + return publicQueuestream; + }, + inject: [EVENT_BUS_TOKEN, PLAYLIST_TOKEN, PREBUFFER_TOKEN] as const, +}); diff --git a/src/providers/queuestream/queuestream.types.ts b/src/providers/queuestream/queuestream.types.ts new file mode 100644 index 0000000..de8e4ce --- /dev/null +++ b/src/providers/queuestream/queuestream.types.ts @@ -0,0 +1,7 @@ +import type { Writable } from 'stream'; + +export type Queuestream = { + getPrebuffer: () => Buffer; + currentPipe: (wrstr: Writable, opts?: { end?: boolean }) => Writable; + next: () => void; +}; diff --git a/src/providers/station.ts b/src/providers/station.ts new file mode 100644 index 0000000..ae8ae06 --- /dev/null +++ b/src/providers/station.ts @@ -0,0 +1,61 @@ +import { declareContainer, injectable } from '@fridgefm/inverter'; +import { createConfig, Config } from './config'; +import { eventBusProvider } from './events/events.provider'; +import { playlistProvider } from './playlist/playlist.provider'; +import { queuestreamProvider } from './queuestream/queuestream.provider'; +import { prebufferProvider } from './prebuffer/prebuffer.provider'; +import { trackProvider } from './track/track.provider'; +import { stationProvider, STATION_PUBLIC_TOKEN } from './station/station.provider'; +import { CONFIG_TOKEN } from './tokens'; + +import type { TStation } from '../types/public.types'; +import type { ReorderCb } from './playlist/playlist.types'; +import type { ClientRequest, ServerResponse } from 'http'; +import type { TEmitter } from './events/events.types'; + +export class Station implements TStation { + private _station: TStation; + + constructor(extConfig?: Partial) { + this._station = declareContainer({ + providers: [ + injectable({ + provide: CONFIG_TOKEN, + useValue: createConfig(extConfig || {}), + }), + trackProvider, + eventBusProvider, + playlistProvider, + prebufferProvider, + queuestreamProvider, + stationProvider, + ], + }).get(STATION_PUBLIC_TOKEN); + } + + public start() { + this._station.start(); + } + + public addFolder(folder: string) { + this._station.addFolder(folder); + } + + public next() { + this._station.next(); + } + + public getPlaylist() { + return this._station.getPlaylist(); + } + + public reorderPlaylist(cb: ReorderCb) { + return this._station.reorderPlaylist(cb); + } + + public connectListener(req: ClientRequest, res: ServerResponse, cb = () => {}) { + this._station.connectListener(req, res, cb); + } + + public on: TEmitter['on'] = (...args) => this._station.on(...args); +} diff --git a/src/providers/station/station.provider.ts b/src/providers/station/station.provider.ts new file mode 100644 index 0000000..4ef17f0 --- /dev/null +++ b/src/providers/station/station.provider.ts @@ -0,0 +1,46 @@ +import { injectable, createToken } from '@fridgefm/inverter'; +import { captureTime } from '../../utils/time'; +import { PLAYLIST_TOKEN } from '../playlist/playlist.provider'; +import { QUEUESTREAM_TOKEN } from '../queuestream/queuestream.provider'; +import { PUBLIC_EVENTS, EVENT_BUS_TOKEN } from '../events/events.provider'; +import { CONFIG_TOKEN } from '../tokens'; + +import type { ClientRequest, ServerResponse } from 'http'; +import type { ReorderCb } from '../playlist/playlist.types'; +import type { TEmitter } from '../events/events.types'; +import type { TStation } from '../../types/public.types'; + +export const STATION_PUBLIC_TOKEN = createToken('station_root'); + +export const stationProvider = injectable({ + provide: STATION_PUBLIC_TOKEN, + useFactory: (config, queuestream, eventBus, playlist) => { + const station = { + start() { + const ct = captureTime(); + + if (this.getPlaylist().length) { + queuestream.next(); + } + + eventBus.emit(PUBLIC_EVENTS.START, this.getPlaylist(), ct()); + }, + getPlaylist: () => playlist.getList(), + addFolder: (folder: string) => playlist.addFolder(folder), + next: () => queuestream.next(), + connectListener(_: ClientRequest, res: ServerResponse, cb = () => {}) { + const { currentPipe, getPrebuffer } = queuestream; + + res.writeHead(200, config.responseHeaders); + res.write(getPrebuffer()); + currentPipe(res); + cb(); + }, + reorderPlaylist: (cb: ReorderCb) => playlist.reorder(cb), + on: (...args: Parameters) => eventBus.on(...args), + }; + + return station; + }, + inject: [CONFIG_TOKEN, QUEUESTREAM_TOKEN, EVENT_BUS_TOKEN, PLAYLIST_TOKEN] as const, +}); diff --git a/src/providers/tokens.ts b/src/providers/tokens.ts new file mode 100644 index 0000000..1814062 --- /dev/null +++ b/src/providers/tokens.ts @@ -0,0 +1,5 @@ +import { createToken } from '@fridgefm/inverter'; + +import type { Config } from './config'; + +export const CONFIG_TOKEN = createToken('config'); diff --git a/src/base/Track/__tests__/methods.test.ts b/src/providers/track/__tests__/track.spec.ts similarity index 96% rename from src/base/Track/__tests__/methods.test.ts rename to src/providers/track/__tests__/track.spec.ts index bfe3877..3eed839 100644 --- a/src/base/Track/__tests__/methods.test.ts +++ b/src/providers/track/__tests__/track.spec.ts @@ -1,8 +1,8 @@ import devnull from 'dev-null'; -import { getStats, getMetaAsync, createSoundStream } from '../methods'; +import { getStats, getMetaAsync, createSoundStream } from '../track.provider'; import { pathToMusic, tracks, TestFile } from '../../../__tests__/test-utils.mock'; -describe('base/Track/methods', () => { +describe('track/methods', () => { it('getStats', () => { const common = { bitrate: 16018, diff --git a/src/base/Track/methods.ts b/src/providers/track/track.provider.ts similarity index 69% rename from src/base/Track/methods.ts rename to src/providers/track/track.provider.ts index 6c00d2b..7461a25 100644 --- a/src/base/Track/methods.ts +++ b/src/providers/track/track.provider.ts @@ -1,15 +1,17 @@ +import { createToken, injectable } from '@fridgefm/inverter'; import fs from 'fs-extra'; import _ from 'highland'; import id3 from 'node-id3'; import { extractLast } from '../../utils/funcs'; import { getDateFromMsecs } from '../../utils/time'; import Mp3 from '../../utils/mp3'; - +import type { TTrack, TrackStats, ShallowTrackMeta, TrackPath } from './track.types'; import type { Readable } from 'stream'; import type { Tags } from 'node-id3'; -import type { ShallowTrackMeta, TrackPath, TrackStats } from './Track.types'; -const getMetaAsync = async (stats: TrackStats): Promise => { +export const TRACK_FACTORY_TOKEN = createToken<(path: string) => TTrack>('track'); + +export const getMetaAsync = async (stats: TrackStats): Promise => { const { fullPath, name } = stats; return new Promise((res) => @@ -30,7 +32,7 @@ const getMetaAsync = async (stats: TrackStats): Promise => { ); }; -const getStats = (fullPath: TrackPath): TrackStats => { +export const getStats = (fullPath: TrackPath): TrackStats => { const file = fs.readFileSync(fullPath); const [directory, fullName] = extractLast(fullPath, '/'); const duration = Mp3.getDuration(file); @@ -51,7 +53,7 @@ const getStats = (fullPath: TrackPath): TrackStats => { }; }; -const createSoundStream = ({ fullPath, bitrate, tagsSize }: TrackStats): [Error | null, Readable] => { +export const createSoundStream = ({ fullPath, bitrate, tagsSize }: TrackStats): [Error | null, Readable] => { try { if (!fs.statSync(fullPath).isFile()) { throw new Error(`Not a file: '${fullPath}'`); @@ -64,7 +66,7 @@ const createSoundStream = ({ fullPath, bitrate, tagsSize }: TrackStats): [Error // @ts-ignore _.drop(Math.floor(tagsSize / bitrate)), // remove id3tags from stream // @ts-ignore - // _.slice(60, 80), // for debuggine purposes + _.slice(60, 80), // for debuggine purposes // @ts-ignore _.ratelimit(1, 1000), ); @@ -77,4 +79,16 @@ const createSoundStream = ({ fullPath, bitrate, tagsSize }: TrackStats): [Error } }; -export { createSoundStream, getMetaAsync, getStats }; +export const trackProvider = injectable({ + provide: TRACK_FACTORY_TOKEN, + useValue: (fullPath: string) => { + const fsStats = getStats(fullPath); + + return { + getMetaAsync: () => getMetaAsync(fsStats), + getSound: () => createSoundStream(fsStats), + fsStats, + playCount: 0, + }; + }, +}); diff --git a/src/base/Track/Track.types.ts b/src/providers/track/track.types.ts similarity index 100% rename from src/base/Track/Track.types.ts rename to src/providers/track/track.types.ts diff --git a/src/types/public.d.ts b/src/types/public.types.ts similarity index 68% rename from src/types/public.d.ts rename to src/types/public.types.ts index 8362acc..e570e50 100644 --- a/src/types/public.d.ts +++ b/src/types/public.types.ts @@ -1,6 +1,6 @@ import type { ClientRequest, ServerResponse } from 'http'; -import type { TrackList, ReorderCb } from '../base/Playlist/Playlist.types'; -import type { TEmitter } from '../features/EventBus/events'; +import type { TrackList, ReorderCb } from '../providers/playlist/playlist.types'; +import type { TEmitter } from '../providers/events/events.types'; export type TStation = { /** @@ -32,3 +32,6 @@ export type TStation = { */ on: TEmitter['on']; }; + +export type { ReorderCb, TrackList, TPlaylist } from '../providers/playlist/playlist.types'; +export type { ShallowTrackMeta, TTrack, TrackStats } from '../providers/track/track.types'; diff --git a/src/utils/__tests__/funcs.test.ts b/src/utils/__tests__/funcs.spec.ts similarity index 100% rename from src/utils/__tests__/funcs.test.ts rename to src/utils/__tests__/funcs.spec.ts diff --git a/tsconfig.json b/tsconfig.json index 3e62072..531c1f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,5 +33,8 @@ }, "include": ["src/**/*"], "exclude": ["node_modules/**", "**/__tests__/"], - "compileOnSave": false + "compileOnSave": false, + "ts-node": { + "files": true + } }