Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: extract thenify and executeWithThenable #229

Merged
merged 1 commit into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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) => {
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')
})
})
})
Loading