Skip to content

Commit

Permalink
chore: extract thenify and executeWithThenable (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
Uzlopak authored Aug 15, 2023
1 parent 0fcb592 commit f94d427
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 45 deletions.
50 changes: 5 additions & 45 deletions boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ const {
} = require('./lib/errors')
const {
kAvvio,
kIsOnCloseHandler,
kThenifyDoNotWrap
kIsOnCloseHandler
} = require('./lib/symbols')
const { TimeTree } = require('./lib/time-tree')
const { Plugin } = require('./lib/plugin')
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 || {}
Expand Down Expand Up @@ -435,58 +436,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) => {
Expand Down
28 changes: 28 additions & 0 deletions lib/executeWithThenable.js
Original file line number Diff line number Diff line change
@@ -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<any>} 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
}
60 changes: 60 additions & 0 deletions lib/thenify.js
Original file line number Diff line number Diff line change
@@ -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
}
82 changes: 82 additions & 0 deletions test/lib/executeWithThenable.test.js
Original file line number Diff line number Diff line change
@@ -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, [])
})
})
123 changes: 123 additions & 0 deletions test/lib/thenify.test.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
})

0 comments on commit f94d427

Please sign in to comment.