diff --git a/core/client.mjs b/core/client.mjs index c541c60..cc06d8d 100644 --- a/core/client.mjs +++ b/core/client.mjs @@ -33,6 +33,8 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals h = https } + let req = null + return new Promise(function(resolve, reject) { if (!path) { return reject(new Error('Request path was empty')) @@ -46,8 +48,6 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals } let timeout = fastRaw ? 5000 : config.timeout || 10000 - let req = null - let timedout = false let timer = setTimeout(function() { timedout = true @@ -103,7 +103,9 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals req.on('error', function(err) { if (timedout) return - reject(new Error(`Error during request ${path}: ${err.message}`)) + let wrapped = new Error(`Error during request ${path}: ${err.message}`) + wrapped.code = err.code + reject(wrapped) }) req.on('timeout', function(err) { if (timedout) return @@ -112,6 +114,9 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals req.end() }).then(function(res) { + if (req) { + req.destroy() + } if (!filePath && !fastRaw) { if (typeof(res.body) === 'string') { try { diff --git a/core/db.mjs b/core/db.mjs index 4198afd..975eb4d 100644 --- a/core/db.mjs +++ b/core/db.mjs @@ -1,143 +1,104 @@ -import lowdb from 'lowdb' -import FileAsync from 'lowdb/adapters/FileAsync.js' +import { Low, JSONFile } from 'lowdb' +import { type } from 'os' -let lastId = -1 +export default function GetDB(util, log, filename = 'db.json') { + let fullpath = util.getPathFromRoot('./' + filename) + const adapter = new JSONFile(fullpath) + const db = new Low(adapter) -// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js -// from package lodash-id -const lodashId = { - // Empties properties - __empty: function (doc) { - this.forEach(doc, function (value, key) { - delete doc[key] - }) - }, + db.id = 'id' - // Copies properties from an object to another - __update: function (dest, src) { - this.forEach(src, function (value, key) { - dest[key] = value - }) - }, - - // Removes an item from an array - __remove: function (array, item) { - var index = this.indexOf(array, item) - if (index !== -1) array.splice(index, 1) - }, - - __id: function () { - var id = this.id || 'id' - return id - }, - - getById: function (collection, id) { - var self = this - return this.find(collection, function (doc) { - if (self.has(doc, self.__id())) { - return doc[self.__id()] === id - } - }) - }, - - createId: function (collection, doc) { - let next = new Date().getTime() - if (next <= lastId) { - next = lastId + 1 + db.createId = function(collection) { + if (collection.length) { + return (collection[collection.length - 1].id || 0) + 1 } - lastId = next - return next - }, - - insert: function (collection, doc) { - doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc) - var d = this.getById(collection, doc[this.__id()]) - if (d) throw new Error('Insert failed, duplicate id') - collection.push(doc) - return doc - }, - - upsert: function (collection, doc) { - if (doc[this.__id()]) { - // id is set - var d = this.getById(collection, doc[this.__id()]) - if (d) { - // replace properties of existing object - this.__empty(d) - this.assign(d, doc) - } else { - // push new object - collection.push(doc) - } - } else { - // create id and push new object - doc[this.__id()] = this.createId(collection, doc) - collection.push(doc) - } - - return doc - }, - - updateById: function (collection, id, attrs) { - var doc = this.getById(collection, id) - - if (doc) { - this.assign(doc, attrs, {id: doc.id}) - } - - return doc - }, - - updateWhere: function (collection, predicate, attrs) { - var self = this - var docs = this.filter(collection, predicate) - - docs.forEach(function (doc) { - self.assign(doc, attrs, {id: doc.id}) - }) - - return docs - }, - - replaceById: function (collection, id, attrs) { - var doc = this.getById(collection, id) - - if (doc) { - var docId = doc.id - this.__empty(doc) - this.assign(doc, attrs, {id: docId}) - } - - return doc - }, - - removeById: function (collection, id) { - var doc = this.getById(collection, id) - - this.__remove(collection, doc) - - return doc - }, - - removeWhere: function (collection, predicate) { - var self = this - var docs = this.filter(collection, predicate) - - docs.forEach(function (doc) { - self.__remove(collection, doc) - }) - - return docs + return 1 } -} -export default function GetDB(util, log) { - const adapter = new FileAsync(util.getPathFromRoot('./db.json')) + db.get = function(collection, id, returnIndex = false) { + let col = db.getCollection(collection) + for (let i = col.length - 1; i >= 0; i--) { + if (col[i][db.id] === id) { + if (returnIndex) return i + return col[i] + } + } + return null + } + + db.getCollection = function(collection) { + if (typeof(collection) === 'string') { + return db.data[collection] + } + return collection + } + + db.upsert = function(collection, item) { + let col = db.getCollection(collection) + if (item[db.id]) { + let i = db.get(col, item[db.id], true) + if (i !== null) { + col[i] = item + return + } + } + item[db.id] = db.createId(col) + col.push(item) + } + + db.remove = function(collection, itemOrId) { + let col = db.getCollection(collection) + let id = itemOrId + if (typeof(id) === 'object') { + id = id[db.id] + } + for (let i = col.length - 1; i >= 0; i--) { + if (col[i][db.id] === id) { + col.splice(i, 1) + return true + } + } + return false + } + + return db.read() + .then(function() { + db.data ||= { + core: { + app: { + active: null, + latestInstalled: null, + latestVersion: null, + versions: [], + }, + manager: { + active: null, + latestInstalled: null, + latestVersion: null, + versions: [], + }, + version: 1, + } + } + + return db.write() + }) + .then( + function() { return db }, + function(err) { + let wrapped = new Error(`Error writing to ${fullpath}: ${err.code}`) + wrapped.code = err.code + throw wrapped + } + ) + + /* + const adapter = new FileAsync(util.getPathFromRoot('./' + filename)) return lowdb(adapter) .then(function(db) { db._.mixin(lodashId) - db.adapterFilePath = util.getPathFromRoot('./db.json') + db.adapterFilePath = util.getPathFromRoot('./' + filename) return db.defaults({ core: { @@ -154,8 +115,9 @@ export default function GetDB(util, log) { }) .write() .then( - function() { db }, - function(e) { log.error(e, 'Error writing defaults to lowdb') } + function() { return db }, + function() { throw new Error('Error writing defaults to lowdb'); } ) }) + */ } diff --git a/core/http.mjs b/core/http.mjs index cecf2b9..dda2d77 100644 --- a/core/http.mjs +++ b/core/http.mjs @@ -1,73 +1,49 @@ import http from 'http' +import https from 'https' export default class HttpServer { constructor(config) { - this.active = { - app: false, - manage: false, - dev: false, + this.ishttps = false + this.active = null + this.sockets = new Set() + this.creator = http + if (config && config.https) { + this.creator = https + this.ishttps = true } - this.sockets = { - app: new Set(), - manage: new Set(), - dev: new Set(), - } - this._context = 'dev' - } - - setContext(name) { - if (name !== 'app' && name !== 'manage' && name !== 'dev') { - throw new Error('Cannot call setContext with values other than app or manage') - } - this._context = name } createServer(opts, listener) { - return this._createServer(this._context, opts, listener) - } - - _createServer(name, opts, listener) { - let server = http.createServer(opts, listener) + let server = this.creator.createServer(opts, listener) server.on('connection', (socket) => { - this.sockets[name].add(socket) + this.sockets.add(socket) socket.once('close', () => { - this.sockets[name].delete(socket) + this.sockets.delete(socket) }) }) - this.active[name] = server + this.active = server return server } - getServer(name) { - return this.active[name] - } + closeServer() { + if (!this.active) return Promise.resolve() - async closeServer(name) { - if (!this.active[name]) return false - - try { - return await new Promise((res, rej) => { - this.sockets[name].forEach(function(socket) { - socket.destroy() - }) - this.sockets[name].clear() - - this.active[name].close(function(err) { - if (err) return rej(err) - - // Waiting 1 second for it to close down - setTimeout(function() { res(true) }, 1000) - }) + return new Promise((res, rej) => { + this.sockets.forEach(function(socket) { + socket.destroy() }) - } catch (err) { - throw new Error(`Error closing ${name}: ${err.message}`) - } - } + this.sockets.clear() - getCurrentServer() { - return this.active[this._context] + this.active.close(err => { + if (err) return rej(err) + this.active = null + + // Waiting 1 second for it to close down + setTimeout(function() {res() }, 1000) + }) + }) } } diff --git a/package.json b/package.json index 30824b5..5f94209 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "bunyan-lite": "^1.0.1", "lodash": "^4.17.20", - "lowdb": "^1.0.0" + "lowdb": "^3.0.0" }, "devDependencies": { "eltro": "^1.3.0" diff --git a/test/db.test.mjs b/test/db.test.mjs index 5a757b3..a556043 100644 --- a/test/db.test.mjs +++ b/test/db.test.mjs @@ -1,2 +1,174 @@ -import { Eltro as t, assert} from 'eltro' +import { Eltro as t, assert, stub } from 'eltro' +import fs from 'fs/promises' import lowdb from '../core/db.mjs' +import Util from '../core/util.mjs' + +var util = new Util(import.meta.url) +var logger = { + info: stub(), + error: stub(), +} + +t.before(function() { + return fs.rm('./test/db_test.json') + .catch(function() {}) +}) + +t.afterEach(function() { + return fs.rm('./test/db_test.json') + .catch(function() { }) +}) + +t.test('Should auto create file with some defaults', async function() { + await assert.isRejected(fs.stat('./test/db_test.json')) + + let db = await lowdb(util, logger, 'db_test.json') + + let stat = await fs.stat('./test/db_test.json') + assert.ok(stat.size > 0) + + assert.ok(db.data.core) + assert.ok(db.data.core.app) + assert.ok(db.data.core.app.versions) + assert.ok(db.data.core.manager) + assert.ok(db.data.core.manager.versions) +}) + +t.test('Should support reading from db', async function() { + const assertValue = { a: 1 } + await assert.isRejected(fs.stat('./test/db_test.json')) + + let db = await lowdb(util, logger, 'db_test.json') + + + db.data.test = assertValue + await db.write() + + assert.strictEqual(db.data.test, assertValue) + + let dbSecondary = await lowdb(util, logger, 'db_test.json') + assert.notStrictEqual(dbSecondary.data.test, assertValue) + assert.deepStrictEqual(dbSecondary.data.test, assertValue) +}) + +t.test('should throw if unable to write to file', function() { + return assert.isRejected(lowdb(util, logger, '../test')) +}) + +t.test('should have basic database-like functions defined', async function() { + const assertItem1 = { a: 1 } + const assertItem2 = { a: 2 } + const assertItem3 = { a: 3 } + let db = await lowdb(util, logger, 'db_test.json') + assert.strictEqual(db.id, 'id') + + db.data.myarr = [] + db.upsert(db.data.myarr, assertItem1) + db.upsert(db.data.myarr, assertItem2) + + assert.strictEqual(db.data.myarr.length, 2) + assert.ok(assertItem1.id) + assert.ok(assertItem2.id) + assert.notStrictEqual(assertItem1.id, assertItem2.id) + + assert.strictEqual(db.get(db.data.myarr, assertItem1.id), assertItem1) + assert.strictEqual(db.get(db.data.myarr, assertItem2.id), assertItem2) + assert.strictEqual(db.get(db.data.myarr, assertItem2.id + 1), null) + + await db.write() + + let dbSec = await lowdb(util, logger, 'db_test.json') + + assert.strictEqual(dbSec.data.myarr.length, 2) + assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1) + assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2) + assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1) + assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2) + + dbSec.upsert(dbSec.data.myarr, assertItem3) + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.ok(assertItem3.id) + assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem3.id), assertItem3) + assert.strictEqual(assertItem2.id + 1, assertItem3.id) + + const assertItem4 = { a: 4, id: assertItem2.id } + dbSec.upsert(dbSec.data.myarr, assertItem4) + + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.notDeepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2) + assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem4) + + const assertItem5 = { a: 5, id: assertItem1.id } + dbSec.upsert(dbSec.data.myarr, assertItem5) + + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.notDeepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1) + assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem5) + + dbSec.remove(dbSec.data.myarr, assertItem2.id) + assert.strictEqual(dbSec.data.myarr.length, 2) + assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), null) + + dbSec.remove(dbSec.data.myarr, assertItem1.id) + assert.strictEqual(dbSec.data.myarr.length, 1) + assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), null) +}) + +t.test('should have basic database-like functions with string-like name of collection', async function() { + const assertItem1 = { a: 1 } + const assertItem2 = { a: 2 } + const assertItem3 = { a: 3 } + let db = await lowdb(util, logger, 'db_test.json') + assert.strictEqual(db.id, 'id') + + db.data.myarr = [] + db.upsert('myarr', assertItem1) + db.upsert('myarr', assertItem2) + + assert.strictEqual(db.data.myarr.length, 2) + assert.ok(assertItem1.id) + assert.ok(assertItem2.id) + assert.notStrictEqual(assertItem1.id, assertItem2.id) + + assert.strictEqual(db.get('myarr', assertItem1.id), assertItem1) + assert.strictEqual(db.get('myarr', assertItem2.id), assertItem2) + assert.strictEqual(db.get('myarr', assertItem2.id + 1), null) + + await db.write() + + let dbSec = await lowdb(util, logger, 'db_test.json') + + assert.strictEqual(dbSec.data.myarr.length, 2) + assert.notStrictEqual(dbSec.get('myarr', assertItem1.id), assertItem1) + assert.notStrictEqual(dbSec.get('myarr', assertItem2.id), assertItem2) + assert.deepEqual(dbSec.get('myarr', assertItem1.id), assertItem1) + assert.deepEqual(dbSec.get('myarr', assertItem2.id), assertItem2) + + dbSec.upsert('myarr', assertItem3) + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.ok(assertItem3.id) + assert.strictEqual(dbSec.get('myarr', assertItem3.id), assertItem3) + assert.strictEqual(assertItem2.id + 1, assertItem3.id) + + const assertItem4 = { a: 4, id: assertItem2.id } + dbSec.upsert('myarr', assertItem4) + + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.notDeepEqual(dbSec.get('myarr', assertItem2.id), assertItem2) + assert.deepEqual(dbSec.get('myarr', assertItem2.id), assertItem4) + + const assertItem5 = { a: 5, id: assertItem1.id } + dbSec.upsert('myarr', assertItem5) + + assert.strictEqual(dbSec.data.myarr.length, 3) + assert.notDeepEqual(dbSec.get('myarr', assertItem1.id), assertItem1) + assert.deepEqual(dbSec.get('myarr', assertItem1.id), assertItem5) + + dbSec.remove('myarr', assertItem2.id) + assert.strictEqual(dbSec.data.myarr.length, 2) + assert.strictEqual(dbSec.get('myarr', assertItem2.id), null) + + dbSec.remove('myarr', assertItem1.id) + assert.strictEqual(dbSec.data.myarr.length, 1) + assert.strictEqual(dbSec.get('myarr', assertItem1.id), null) +}) diff --git a/test/http.test.mjs b/test/http.test.mjs new file mode 100644 index 0000000..c713d5e --- /dev/null +++ b/test/http.test.mjs @@ -0,0 +1,133 @@ +import { Eltro as t, assert, stub } from 'eltro' +import http from 'http' +import https from 'https' +import { request } from '../core/client.mjs' +import HttpServer from '../core/http.mjs' + +const port = 61413 +let prefix = `http://localhost:${port}/` + +t.describe('config', function() { + t.test('should use https if https is true', function() { + let server = new HttpServer() + assert.strictEqual(server.creator, http) + assert.strictEqual(server.ishttps, false) + server = new HttpServer({ https: true }) + assert.strictEqual(server.creator, https) + assert.strictEqual(server.ishttps, true) + }) +}) + +t.describe('Sockets', function() { + let http = new HttpServer() + + t.after(function() { + http.closeServer().then(function() { }, function(err) { + console.error(err) + }) + }) + + t.test('should keep track of sockets through its lifetime', function(cb) { + let actives = [] + + let server = http.createServer(function(req, res) { + req.on('error', function(err) { cb(err) }) + res.on('error', function(err) { cb(err) }) + res.on('finish', function() { }) + + actives.push(res) + }) + + Promise.resolve() + .then(async function() { + await new Promise(function(res, rej) { + server.listen(port, function(err) { if (err) rej(err); res()}) + }) + + assert.strictEqual(actives.length, 0) + assert.strictEqual(http.sockets.size, 0) + + request({}, prefix).then(function() {}, cb) + request({}, prefix).then(async function() { + while (http.sockets.size > 0) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + assert.strictEqual(http.sockets.size, 0) + cb() + }, cb) + + while (actives.length < 2) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + assert.strictEqual(http.sockets.size, 2) + actives[0].statusCode = 200 + actives[0].end('{}') + actives[1].statusCode = 200 + actives[1].end('{}') + }).catch(cb) + }) +}) + +t.describe('Close', function() { + let http = new HttpServer() + + t.after(function() { + http.closeServer().then(function() { }, function(err) { + console.error(err) + }) + }) + + t.test('should support forcefully closing them on server close', function(cb) { + let requestErrors = [] + let serverErrors = [] + + let server = http.createServer(function(req, res) { + req.on('error', function(err) { serverErrors.push(err) }) + res.on('error', function(err) { serverErrors.push(err) }) + res.on('finish', function() { }) + }) + + Promise.resolve() + .then(async function() { + await new Promise(function(res, rej) { + server.listen(port, function(err) { if (err) rej(err); res()}) + }) + + assert.strictEqual(http.sockets.size, 0) + + request({}, prefix).then( + function() { cb(new Error('first succeeded')) }, + function(err) { requestErrors.push(err) } + ) + request({}, prefix).then( + function() { cb(new Error('first succeeded')) }, + function(err) { requestErrors.push(err) } + ) + + while (http.sockets.size < 2) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + + http.closeServer().then(function() { }, cb) + + while (requestErrors.length < 2) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + assert.strictEqual(http.sockets.size, 0) + assert.strictEqual(requestErrors.length, 2) + assert.strictEqual(serverErrors.length, 2) + assert.strictEqual(serverErrors[0].code, 'ECONNRESET') + assert.strictEqual(serverErrors[1].code, 'ECONNRESET') + assert.strictEqual(requestErrors[0].code, 'ECONNRESET') + assert.strictEqual(requestErrors[1].code, 'ECONNRESET') + + while (requestErrors.length < 2) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + while (http.active) { + await new Promise(function(res) { setTimeout(res, 10) }) + } + }) + .then(function() { cb()}, cb) + }) +}) diff --git a/test/util.test.mjs b/test/util.test.mjs index 61ceaaf..1e6487e 100644 --- a/test/util.test.mjs +++ b/test/util.test.mjs @@ -29,10 +29,6 @@ t.describe('#getPathFromRoot()', function() { t.describe('#getUrlFromRoot()', function() { t.test('should return an import compatible path', async function() { var util = new Util(import.meta.url) - let err = await assert.isRejected(import(util.getPathFromRoot('template.mjs'))) - assert.match(err.message, /ESM/i) - assert.match(err.message, /file/i) - assert.match(err.message, /data/i) let data = await import(util.getUrlFromRoot('template.mjs')) assert.deepEqual(data.default, { a: 1 })