Thenable interface and conformance test case
This has been carefully balanced between the requirements of Promises/A+ and the abilitiy for implementations to provide their own flexible use cases for a polymorphic API; it will evolve as needed until the first release. The conformance test cases do pass properly---I will be comitting an implementation that makes use of the conformance tests.aplus
parent
d25dff1e84
commit
0ff65e6fd6
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* Promises/A+ "thenable" type
|
||||
*
|
||||
* Copyright (C) 2014 Mike Gerwitz
|
||||
*
|
||||
* This file is part of jsTonic.
|
||||
*
|
||||
* jstonic is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* There are a lot of words here, and very little code; to ensure that you
|
||||
* get the most from your implementation of this interface, be sure to also
|
||||
* run the Thenable conformance tests on it.
|
||||
*/
|
||||
|
||||
let Interface = require( 'easejs' ).Interface;
|
||||
|
||||
|
||||
/**
|
||||
* Any type defining a #then method with two parameters for, respectfully,
|
||||
* asynchronous request fulfillment and rejection
|
||||
*
|
||||
* This formalizes "thenable" as defined in Promises/A+ in §2.2, but
|
||||
* decoupled from the requirements of a "promise". When the term "thenable"
|
||||
* is used, it refers to the Promises/A+ specification---any function or
|
||||
* object with a `then` method; when `Thenable` (with a capital 'T') is
|
||||
* used, it refers to this interface or an object of type `Thenable`.
|
||||
*
|
||||
* The distinction between a `Thenable` and a promise is intentional: a
|
||||
* `Thenable` has certain guarantees ensuring that it can be used
|
||||
* predictably (and polymorphically) as part of a promise-like pattern; it
|
||||
* says nothing of how a `Thenable` is resolved (fulfilled or rejected), for
|
||||
* instance.
|
||||
*
|
||||
* `Thenable`s are *not* required to be stateful and may therefore be
|
||||
* implemented deterministically; that is---a `Thenable` need not keep track
|
||||
* of prior invocations to prevent duplicate callback applications (indeed,
|
||||
* this specification discourages stateful `Thenable`s). This can be
|
||||
* rationalized in a number of ways, all conforming to the language and
|
||||
* spirit of the Promises/A+ specification:
|
||||
*
|
||||
* 1. Section 1.2 defines a "thenable" as: "an object or function that
|
||||
* defines a `then` method"; this is a very broad definition that does
|
||||
* not even impose parameter requirements (but `Thenable` does);
|
||||
*
|
||||
* 2. Section 1.1 defines a "promise" as: "an object or function with a
|
||||
* `then` method that *conforms to this specification*" (emphasis
|
||||
* added); note that 1.2 has no requirement to conform to any
|
||||
* specification;
|
||||
*
|
||||
* 3. States are imposed upon "promises" by §2.1---these states are what
|
||||
* prevent invoking `then` callbacks multiple times, because
|
||||
* Promises/A+ requires that a promise (a) cannot change state once
|
||||
* resolved; and (b) cannot change its value; from this we infer that
|
||||
* `Thenable`s need not manage such state;
|
||||
*
|
||||
* 4. Paragraphs 2.2.{2,3}.3 do require that each callback "not be called
|
||||
* more than once", but this requirement is for a `then` method that
|
||||
* conforms to the Promises/A+ specification---a requirement imposed
|
||||
* not on a thenable, but instead upon a promise (by §1); and
|
||||
*
|
||||
* 5. Paragraph 2.3.3.3 describes invoking an arbitrary thenable, making
|
||||
* no mention of state, and instead relying on the state of the promise
|
||||
* being resolved, which wraps the result of the thenable. Indeed, the
|
||||
* opening paragraphs of §2.3 include: "It also allows the
|
||||
* Promises/A+ implementations to "assimilate" nonconformant
|
||||
* implementations with reasonable `then` methods". `Thenable` ensures
|
||||
* that such methods are reasonable through its specification and,
|
||||
* notably, parameter requirements (which are compatible with 2.3.3.3).
|
||||
*
|
||||
* `Thenable` therefore permits arbitrary implementations---possibly
|
||||
* entirely disjoint from the concept of promises---while still allowing
|
||||
* them to work with the A+ implementation by imposing a *subset* of
|
||||
* requirements of the Promises/A+ specification. This is important, since
|
||||
* it would be ideal for a consumer of a promise to instead accept anything
|
||||
* of type Thenable, confident that the callbacks will be invoked
|
||||
* consistently and as intended, without having to worry about any
|
||||
* particular promise implementation (thereby successfully remaining
|
||||
* decoupled).
|
||||
*
|
||||
* The asynchronous requirement (imposed by the stack requirements in
|
||||
* Promises/A+) of `Thenable` exists because it does not make much sense to
|
||||
* use a synchronous thenable---otherwise, no `then` method would be needed,
|
||||
* and the code should be written in a synchronous style; it is dangerous
|
||||
* for code to be written in an asynchronous style and to then be executed
|
||||
* synchronously, as it may have disastrous effects on system responsiveness
|
||||
* (consider UI updates that use `then` as an implicit opportunity to
|
||||
* repaint).
|
||||
*
|
||||
* Of course, this interface specification must be followed in order for
|
||||
* such a point to be valid; just be cognisant of the implementations that
|
||||
* you accept in your own software. This interface will accept any thenable
|
||||
* as described by Promises/A+---even plain objects with a `then`
|
||||
* method---through GNU ease.js' interop support, when the object in
|
||||
* question is not an instance of an ease.js class.
|
||||
*
|
||||
* For examples---and to test your own Thenable implementations---refer to
|
||||
* the conformance tests.
|
||||
*/
|
||||
module.exports = Interface( 'Thenable',
|
||||
{
|
||||
/**
|
||||
* Access current or eventual value or reason
|
||||
*
|
||||
* Both callbacks are optional and shall be either ignored or rejected
|
||||
* if they are not functions, and shall be silently ignored if
|
||||
* missing (length of argument list is less than two), `undefined` or
|
||||
* `null`. Only one of either ONFULFILLED or ONREJECTED shall ever be
|
||||
* invoked for a given value/reason pair.
|
||||
*
|
||||
* The callbacks must be executed after the call stack has cleared and
|
||||
* contains only calls to platform code---in practice, this means
|
||||
* executing after a setTimeout, process.nextTick, or something similar.
|
||||
* They must be invoked as functions with no `this` value.
|
||||
*
|
||||
* This interface decouples thenables from the concept of a promise
|
||||
* (contrary to Promises/A+ §2.2); therefore, the return value
|
||||
* must return another Thenable, but any more specific implementation
|
||||
* details are undefined; specific implementations may impose additional
|
||||
* requirements.
|
||||
*
|
||||
* This method may be called multiple times for any `Thenable`, in which
|
||||
* case all callbacks must be enqueued for invocation in the order that
|
||||
* their respective `then` was called (Promises/A+ §2.2.6).
|
||||
*
|
||||
* @param {function(*)} onFulfilled invoked upon value fulfillment
|
||||
* @param {function(*)} onRejected invoked upon value rejection/failure
|
||||
*
|
||||
* @return {Thenable} another Thenable object
|
||||
*/
|
||||
then: [ 'onFulfilled', 'onRejected' ],
|
||||
} );
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* Tests for conforming Thenable implementation
|
||||
*
|
||||
* Copyright (C) 2014 Mike Gerwitz
|
||||
*
|
||||
* This file is part of jsTonic.
|
||||
*
|
||||
* jstonic is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
let Class = require( 'easejs' ).Class,
|
||||
Sut = require( '../../' ).then.Thenable,
|
||||
expect = require( 'chai' ).expect;
|
||||
|
||||
|
||||
module.exports = ( ctor, accept, reject ) =>
|
||||
{
|
||||
describe( 'conforming to Thenable', () =>
|
||||
{
|
||||
it( 'implements Thenable', () =>
|
||||
expect( Class.isA( Sut, ctor() ) ).to.be.true );
|
||||
|
||||
|
||||
describe( '#then', () => _thenTests( ctor, accept, reject ) );
|
||||
|
||||
|
||||
/**
|
||||
* Promises/A+ 2.2.4.
|
||||
*
|
||||
* For detailed rationale on why this is imposed on a simple
|
||||
* thenable, see the Thenable interface.
|
||||
*/
|
||||
describe( 'call stack', () =>
|
||||
describe( 'is allowed to clear', () =>
|
||||
{
|
||||
let stack_ctor = ( type, done ) =>
|
||||
{
|
||||
let cleared = false;
|
||||
|
||||
let chk = () =>
|
||||
{
|
||||
expect( cleared ).to.be.true;
|
||||
done();
|
||||
};
|
||||
|
||||
// ensure the stack clears
|
||||
setTimeout( () => cleared = true, 0 );
|
||||
|
||||
// if the callback is invoked asynchronously, then it
|
||||
// will run after the above timeout callback
|
||||
type( ctor().then(
|
||||
type === accept && chk,
|
||||
type === reject && chk ) );
|
||||
};
|
||||
|
||||
it( 'before invoking fulfillment callback', done =>
|
||||
stack_ctor( accept, done ) );
|
||||
|
||||
it( 'before invoking rejection callback', done =>
|
||||
stack_ctor( reject, done ) );
|
||||
} ) );
|
||||
} );
|
||||
};
|
||||
|
||||
|
||||
function _thenTests( ctor, accept, reject )
|
||||
{
|
||||
let sut;
|
||||
|
||||
// these methods just make it easier to work into a lisp-style
|
||||
// procedure
|
||||
let accept_sut = () => accept( sut ),
|
||||
reject_sut = () => reject( sut );
|
||||
|
||||
beforeEach( () =>
|
||||
{
|
||||
sut = ctor();
|
||||
} );
|
||||
|
||||
|
||||
it( 'returns another Thenable', () =>
|
||||
expect( Class.isA( Sut, ctor().then( null, null ) ) )
|
||||
.to.be.true );
|
||||
|
||||
|
||||
describe( 'callbacks', () =>
|
||||
{
|
||||
/**
|
||||
* The Promises/A+ specification states that non-function
|
||||
* arguments must be "ignored"; this is a bit ambiguous, since
|
||||
* ignoring can involve any number of things, including throwing
|
||||
* an exception (and ignoring the values as a consequence of
|
||||
* aborting the call).
|
||||
*
|
||||
* We will allow the implementation to do whatever it chooses,
|
||||
* provided that the most definitive indications of intentional
|
||||
* omission are permitted without error.
|
||||
*
|
||||
* See Promises/A+ §2.2.1.
|
||||
*/
|
||||
describe( 'params', () =>
|
||||
{
|
||||
it( 'permit omitted callbacks', () =>
|
||||
expect( () => ctor().then() )
|
||||
.to.not.throw( Error ) );
|
||||
|
||||
it( 'permit undefined callbacks', () =>
|
||||
expect( () => ctor().then( undefined, undefined ) )
|
||||
.to.not.throw( Error ) );
|
||||
|
||||
it( 'permit null callbacks', () =>
|
||||
expect( () => ctor().then( null, null ) )
|
||||
.to.not.throw( Error ) );
|
||||
} );
|
||||
|
||||
|
||||
it( 'trigger fulfillment on accept', done =>
|
||||
accept_sut( sut.then( ()=>done() ) ) );
|
||||
|
||||
it( 'trigger rejection on reject', done =>
|
||||
reject_sut( sut.then( null, ()=>done() ) ) );
|
||||
|
||||
|
||||
/**
|
||||
* An implementation that invokes both callbacks for a given
|
||||
* value/reason pair (that is---either an accept or reject) is
|
||||
* implying that the request both succeeded and failed; since we
|
||||
* have no intent on supporting superimposed states---and it's
|
||||
* rational to assume that the consumer of the implementation
|
||||
* has no such intent either---such an implementation should be
|
||||
* considered to be in error.
|
||||
*
|
||||
* This will obviously not catch cases that may require certain
|
||||
* types of input (e.g. a bug in specific portions of the
|
||||
* logic), but will hopefully help to point out fatal design
|
||||
* flaws, and prevent the need to duplicate such obvious tests
|
||||
* for every `Thenable` implementation.
|
||||
*/
|
||||
describe( 'are not both called for a value/reason pair', () =>
|
||||
{
|
||||
/**
|
||||
* This implementation attempts to determine, broadly,
|
||||
* whether both callbacks were invoked. Doing so is not
|
||||
* fool-proof, since `Thenable` mandates asynchrony, but we
|
||||
* have no idea when the asynchronous event will be
|
||||
* scheduled. Here, we assume that the callbacks are
|
||||
* scheduled immediately after the invocation of `accept` or
|
||||
* `reject`, and that scheduling our own callback would
|
||||
* enqueue it after any `Thenable` callbacks. This may or
|
||||
* may not be true.
|
||||
*/
|
||||
let postchk = ( f, done ) =>
|
||||
{
|
||||
let ok = true,
|
||||
cf = () => ok = false;
|
||||
|
||||
f( ctor().then(
|
||||
( f === reject ) && cf,
|
||||
( f === accept ) && cf ) );
|
||||
|
||||
setTimeout( () => {
|
||||
expect( ok ).to.be.true;
|
||||
done();
|
||||
}, 0 );
|
||||
};
|
||||
|
||||
it( 'when fulfilled', done =>
|
||||
postchk( accept, done ) );
|
||||
|
||||
it( 'when rejected', done =>
|
||||
postchk( reject, done ) );
|
||||
} );
|
||||
|
||||
|
||||
/**
|
||||
* Promises/A+ §2.2.5.
|
||||
*/
|
||||
describe( 'calling context', () =>
|
||||
{
|
||||
let no_context = ( () => this )();
|
||||
|
||||
let expect_no_c = done =>
|
||||
() => expect( this ).to.equal( no_context )
|
||||
&& done();
|
||||
|
||||
it( 'invokes fulfilled callback without context', done =>
|
||||
accept_sut( sut
|
||||
.then( expect_no_c( ()=>done() ), null ) ) );
|
||||
|
||||
it( 'invokes reject callback without context', done =>
|
||||
reject_sut( sut
|
||||
.then( null, expect_no_c( ()=>done() ) ) ) );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
describe( 'invoked multiple times', () =>
|
||||
/**
|
||||
* There may be more than one observer interested in the results
|
||||
* of a computation; Promises/A+ §2.2.6 states that each
|
||||
* callback should be invoked in the order of their respective
|
||||
* `then` calls.
|
||||
*/
|
||||
describe( 'will invoke in order of respective #then', () =>
|
||||
{
|
||||
let c = ( chk, i ) => {
|
||||
expect( chk[ i-1 ] ).to.be.undefined;
|
||||
chk[ i ] = undefined;
|
||||
};
|
||||
|
||||
let multi_ctor = done =>
|
||||
{
|
||||
let chk = [ 0, 1, 2 ];
|
||||
|
||||
chk.forEach( i =>
|
||||
{
|
||||
let f = c( chk, i );
|
||||
sut.then( f, f );
|
||||
} );
|
||||
|
||||
sut.then( ()=>done(), ()=>done() );
|
||||
return sut;
|
||||
};
|
||||
|
||||
it( 'all fulfillment callbacks upon accept', done =>
|
||||
accept_sut( multi_ctor( done ) ) );
|
||||
|
||||
it( 'all rejection callbacks upon reject', done =>
|
||||
reject_sut( multi_ctor( done ) ) );
|
||||
} ) );
|
||||
}
|
||||
|
Loading…
Reference in New Issue