From 17c3c0d1fe67af6b24d9067ae5c9f02fdf9df80f Mon Sep 17 00:00:00 2001 From: uzlopak Date: Mon, 14 Aug 2023 04:41:04 +0200 Subject: [PATCH] chore: extract thenify and executeWithThenable --- boot.js | 50 ++--------- lib/executeWithThenable.js | 28 ++++++ lib/thenify.js | 60 +++++++++++++ test/lib/executeWithThenable.test.js | 82 ++++++++++++++++++ test/lib/thenify.test.js | 123 +++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 45 deletions(-) create mode 100644 lib/executeWithThenable.js create mode 100644 lib/thenify.js create mode 100644 test/lib/executeWithThenable.test.js create mode 100644 test/lib/thenify.test.js diff --git a/boot.js b/boot.js index 5c1001c..85bc3fc 100644 --- a/boot.js +++ b/boot.js @@ -11,8 +11,7 @@ const { } = require('./lib/errors') const { kAvvio, - kIsOnCloseHandler, - kThenifyDoNotWrap + kIsOnCloseHandler } = require('./lib/symbols') const { TimeTree } = require('./lib/time-tree') const { Plugin } = require('./lib/plugin') @@ -20,6 +19,8 @@ const { debug } = require('./lib/debug') const { validatePlugin } = require('./lib/validate-plugin') const { isBundledOrTypescriptPlugin } = require('./lib/is-bundled-or-typescript-plugin') const { isPromiseLike } = require('./lib/is-promise-like') +const { thenify } = require('./lib/thenify') +const { executeWithThenable } = require('./lib/executeWithThenable') function wrap (server, opts, instance) { const expose = opts.expose || {} @@ -437,58 +438,17 @@ Boot.prototype._loadPluginNextTick = function (plugin, callback) { function noop () { } -function thenify () { - // If the instance is ready, then there is - // nothing to await. This is true during - // await server.ready() as ready() resolves - // with the server, end we will end up here - // because of automatic promise chaining. - if (this.booted) { - debug('thenify returning null because we are already booted') - return - } - - // Calling resolve(this._server) would fetch the then - // property on the server, which will lead it here. - // If we do not break the recursion, we will loop - // forever. - if (this[kThenifyDoNotWrap]) { - this[kThenifyDoNotWrap] = false - return - } - - debug('thenify') - return (resolve, reject) => { - const p = this._loadRegistered() - return p.then(() => { - this[kThenifyDoNotWrap] = true - return resolve(this._server) - }, reject) - } -} - function callWithCbOrNextTick (func, cb) { const context = this._server const err = this._error - let res // with this the error will appear just in the next after/ready callback this._error = null if (func.length === 0) { this._error = err - res = func() - if (isPromiseLike(res) && !res[kAvvio]) { - res.then(() => process.nextTick(cb), (e) => process.nextTick(cb, e)) - } else { - process.nextTick(cb) - } + executeWithThenable(func, [], cb) } else if (func.length === 1) { - res = func(err) - if (isPromiseLike(res) && !res[kAvvio]) { - res.then(() => process.nextTick(cb), (e) => process.nextTick(cb, e)) - } else { - process.nextTick(cb) - } + executeWithThenable(func, [err], cb) } else { if (this._timeout === 0) { const wrapCb = (err) => { diff --git a/lib/executeWithThenable.js b/lib/executeWithThenable.js new file mode 100644 index 0000000..6e2c809 --- /dev/null +++ b/lib/executeWithThenable.js @@ -0,0 +1,28 @@ +'use strict' +const { isPromiseLike } = require('./is-promise-like') +const { kAvvio } = require('./symbols') + +/** + * @callback ExecuteWithThenableCallback + * @param {Error} error + * @returns {void} + */ + +/** + * @param {Function} func + * @param {Array} args + * @param {ExecuteWithThenableCallback} [callback] + */ +function executeWithThenable (func, args, callback) { + const result = func.apply(func, args) + if (isPromiseLike(result) && !result[kAvvio]) { + // process promise but not avvio mock thenable + result.then(() => process.nextTick(callback), (error) => process.nextTick(callback, error)) + } else if (callback) { + process.nextTick(callback) + } +} + +module.exports = { + executeWithThenable +} diff --git a/lib/thenify.js b/lib/thenify.js new file mode 100644 index 0000000..e1b614d --- /dev/null +++ b/lib/thenify.js @@ -0,0 +1,60 @@ +'use strict' + +const { debug } = require('./debug') +const { kThenifyDoNotWrap } = require('./symbols') + +/** + * @callback PromiseConstructorLikeResolve + * @param {any} value + * @returns {void} + */ + +/** + * @callback PromiseConstructorLikeReject + * @param {reason} error + * @returns {void} + */ + +/** + * @callback PromiseConstructorLike + * @param {PromiseConstructorLikeResolve} resolve + * @param {PromiseConstructorLikeReject} reject + * @returns {void} + */ + +/** + * @returns {PromiseConstructorLike} + */ +function thenify () { + // If the instance is ready, then there is + // nothing to await. This is true during + // await server.ready() as ready() resolves + // with the server, end we will end up here + // because of automatic promise chaining. + if (this.booted) { + debug('thenify returning undefined because we are already booted') + return + } + + // Calling resolve(this._server) would fetch the then + // property on the server, which will lead it here. + // If we do not break the recursion, we will loop + // forever. + if (this[kThenifyDoNotWrap]) { + this[kThenifyDoNotWrap] = false + return + } + + debug('thenify') + return (resolve, reject) => { + const p = this._loadRegistered() + return p.then(() => { + this[kThenifyDoNotWrap] = true + return resolve(this._server) + }, reject) + } +} + +module.exports = { + thenify +} diff --git a/test/lib/executeWithThenable.test.js b/test/lib/executeWithThenable.test.js new file mode 100644 index 0000000..24fa81a --- /dev/null +++ b/test/lib/executeWithThenable.test.js @@ -0,0 +1,82 @@ +'use strict' + +const { test } = require('tap') +const { executeWithThenable } = require('../../lib/executeWithThenable') +const { kAvvio } = require('../../lib/symbols') + +test('executeWithThenable', (t) => { + t.plan(6) + + t.test('passes the arguments to the function', (t) => { + t.plan(5) + + executeWithThenable((...args) => { + t.equal(args.length, 3) + t.equal(args[0], 1) + t.equal(args[1], 2) + t.equal(args[2], 3) + }, [1, 2, 3], (err) => { + t.error(err) + }) + }) + + t.test('function references this to itself', (t) => { + t.plan(2) + + const func = function () { + t.equal(this, func) + } + executeWithThenable(func, [], (err) => { + t.error(err) + }) + }) + + t.test('handle resolving Promise of func', (t) => { + t.plan(1) + + const fn = function () { + return Promise.resolve(42) + } + + executeWithThenable(fn, [], (err) => { + t.error(err) + }) + }) + + t.test('handle rejecting Promise of func', (t) => { + t.plan(1) + + const fn = function () { + return Promise.reject(new Error('Arbitrary Error')) + } + + executeWithThenable(fn, [], (err) => { + t.equal(err.message, 'Arbitrary Error') + }) + }) + + t.test('dont handle avvio mocks PromiseLike results but use callback if provided', (t) => { + t.plan(1) + + const fn = function () { + const result = Promise.resolve(42) + result[kAvvio] = true + } + + executeWithThenable(fn, [], (err) => { + t.error(err) + }) + }) + + t.test('dont handle avvio mocks Promises and if no callback is provided', (t) => { + t.plan(1) + + const fn = function () { + t.pass(1) + const result = Promise.resolve(42) + result[kAvvio] = true + } + + executeWithThenable(fn, []) + }) +}) diff --git a/test/lib/thenify.test.js b/test/lib/thenify.test.js new file mode 100644 index 0000000..b0c0c95 --- /dev/null +++ b/test/lib/thenify.test.js @@ -0,0 +1,123 @@ +'use strict' + +const { test, mock } = require('tap') +const { kThenifyDoNotWrap } = require('../../lib/symbols') + +test('thenify', (t) => { + t.plan(7) + + t.test('return undefined if booted', (t) => { + t.plan(2) + + const { thenify } = mock('../../lib/thenify', { + '../../lib/debug': { + debug: (message) => { t.equal(message, 'thenify returning undefined because we are already booted') } + } + }) + const result = thenify.call({ + booted: true + }) + t.equal(result, undefined) + }) + + t.test('return undefined if kThenifyDoNotWrap is true', (t) => { + t.plan(1) + + const { thenify } = require('../../lib/thenify') + const result = thenify.call({ + [kThenifyDoNotWrap]: true + }) + t.equal(result, undefined) + }) + + t.test('return PromiseConstructorLike if kThenifyDoNotWrap is false', (t) => { + t.plan(3) + + const { thenify } = mock('../../lib/thenify', { + '../../lib/debug': { + debug: (message) => { t.equal(message, 'thenify') } + } + }) + const promiseContructorLike = thenify.call({ + [kThenifyDoNotWrap]: false + }) + + t.type(promiseContructorLike, 'function') + t.equal(promiseContructorLike.length, 2) + }) + + t.test('return PromiseConstructorLike', (t) => { + t.plan(3) + + const { thenify } = mock('../../lib/thenify', { + '../../lib/debug': { + debug: (message) => { t.equal(message, 'thenify') } + } + }) + const promiseContructorLike = thenify.call({}) + + t.type(promiseContructorLike, 'function') + t.equal(promiseContructorLike.length, 2) + }) + + t.test('resolve should return _server', async (t) => { + t.plan(1) + + const { thenify } = require('../../lib/thenify') + + const server = { + _loadRegistered: () => { + return Promise.resolve() + }, + _server: 'server' + } + const promiseContructorLike = thenify.call(server) + + promiseContructorLike(function (value) { + t.equal(value, 'server') + }, function (reason) { + t.error(reason) + }) + }) + + t.test('resolving should set kThenifyDoNotWrap to true', async (t) => { + t.plan(1) + + const { thenify } = require('../../lib/thenify') + + const server = { + _loadRegistered: () => { + return Promise.resolve() + }, + [kThenifyDoNotWrap]: false, + _server: 'server' + } + const promiseContructorLike = thenify.call(server) + + promiseContructorLike(function (value) { + t.equal(server[kThenifyDoNotWrap], true) + }, function (reason) { + t.error(reason) + }) + }) + + t.test('rejection should pass through to reject', async (t) => { + t.plan(1) + + const { thenify } = require('../../lib/thenify') + + const server = { + _loadRegistered: () => { + return Promise.reject(new Error('Arbitrary rejection')) + }, + _server: 'server' + } + const promiseContructorLike = thenify.call(server) + + promiseContructorLike(function (value) { + t.error(value) + }, function (reason) { + t.equal(reason.message, 'Arbitrary rejection') + }) + }) +})