Skip to content

Commit

Permalink
feat(deferSet): Added deferSet feature
Browse files Browse the repository at this point in the history
  • Loading branch information
grantila committed Aug 18, 2019
1 parent 84a55da commit b1e27a1
Show file tree
Hide file tree
Showing 6 changed files with 450 additions and 157 deletions.
45 changes: 45 additions & 0 deletions README.md
Expand Up @@ -31,6 +31,7 @@ This library is written in TypeScript but is exposed as ES7 (if imported as `alr
* [once](#once)
* [retry](#retry)
* [defer](#defer)
* [deferSet](#deferset)
* [reflect](#reflect)
* [inspect](#inspect)
* [Try](#try)
Expand Down Expand Up @@ -422,6 +423,50 @@ deferred.resolve( ); // This is now legal, typewise
```


## deferSet

Instead of creating a lot of defer objects, e.g. in unit tests to trigger asynchrony in a certain order, `deferSet` is a cleaner way.

A *"defer set"* is a dynamically growable set of indexes (numbers) which can be awaited, resolved or rejected at any time.

`deferSet( )` returns an object (of a class `OrderedAsynchrony`). This has the helper functions:

* `wait( index | [indices...] ) -> Promise< void >`
* `resolve( index | [indices...] ) -> Promise< void >`
* `reject( index | [indices...] ) -> Promise< void >`

```ts
import { deferSet } from 'already'

const order = deferSet( );

order.resolve( 0 ); // Resolve index 0
await order.wait( 0 ); // Wait for promise 0 (which was resolved above)
```

The above will work fine, it's basically creating a `defer`, resolving it and then awaiting its promise. This will deadlock:

```ts
await order.wait( 0 ); // Will wait forever
order.resolve( 0 );
```

It's possible to wait, resolve and reject multiple indices at once, by specifying an array instead. And `wait` can take an optional index (or array of indices) to resolve, as well as an optional index (or array of indices) to reject.

The return value of `wait( )`, `resolve( )` and `reject( )` is a promise *and* the defer set itself.

```ts
// Do stuff, and eventually trigger certain index resolutions.
doFoo( ).then( ( ) => { order.resolve( 0 ); } ); // Eventually resolves index 0
doBar( ).then( ( ) => { order.resolve( [ 1, 3 ] ); } ); // Eventually resolves index 1 and 3
// etc.

await order.wait( [ 0, 1, 3 ], 2 ); // Await index 0, 1 and 3, resolve index 2.
order.reject( 4 ); // Will reject index 4 with an error.
await order.wait( 4 ); // Will (asynchronously) throw.
```


## reflect

A promise can be either resolved or rejected, but sometimes it's convenient to have a shared flow for either occasion. That's when `reflect` comes in handy. It takes a promise as argument, and returns a promise to a `Reflection` object which contains the *value* **or** *error*, and the booleans `isResolved` and `isRejected`.
Expand Down
3 changes: 2 additions & 1 deletion jest.config.js
Expand Up @@ -9,9 +9,10 @@ module.exports = {
modulePathIgnorePatterns: [
".*\.d\.ts"
],
collectCoverageFrom: ["<rootDir>/lib/**", "index.ts"],
collectCoverageFrom: ["<rootDir>/dist/**/*.js"],
coverageReporters: ["lcov", "text", "html"],
setupFiles: [
"trace-unhandled/register",
],
maxConcurrency: Infinity,
};
116 changes: 116 additions & 0 deletions lib/index.ts
Expand Up @@ -4,6 +4,7 @@ export default {
Finally,
Try,
defer,
deferSet,
delay,
delayChain,
each,
Expand Down Expand Up @@ -1172,3 +1173,118 @@ export function funnel< T, U extends Promise< T > = Promise< T > >(
return runner( fn, [ ] );
};
}


export class OrderedAsynchrony
{
private deferrals: Array< EmptyDeferred > = [ ];

public wait(
waitForIndex: number | ConcatArray< number >,
resolveIndex?: number | ConcatArray< number > | undefined | null,
rejectIndex?: number | ConcatArray< number > | undefined | null
)
: Promise< void > & this
{
this.ensureDeferral( [
...( ( < Array< number > >[ ] ).concat( waitForIndex ) ),
...(
resolveIndex == null ? [ ] :
( < Array< number > >[ ] ).concat( resolveIndex )
),
...(
rejectIndex == null ? [ ] :
( < Array< number > >[ ] ).concat( rejectIndex )
),
] );

return this.decorate(
Promise.all(
( < Array< number > >[ ] ).concat( waitForIndex )
.map( index => this.deferrals[ index ].promise )
)
.then( ( ) =>
Promise.all( [
resolveIndex == null
? void 0
: this.resolve( resolveIndex ),
rejectIndex == null
? void 0
: this.reject( rejectIndex ),
] )
.then( ( ) => { } )
)
);
}

public resolve( index: number | ConcatArray< number > )
: Promise< void > & this
{
this.ensureDeferral( index );

return this.decorate( delay( 0 ).then( ( ) =>
{
( < Array< number > >[ ] ).concat( index )
.forEach( index =>
{
this.deferrals[ index ].resolve( );
} );
} ) );
}

public reject(
index: number | ConcatArray< number >,
error = new Error( "OrderedAsynchrony rejection" )
)
: Promise< void > & this
{
this.ensureDeferral( index );

return this.decorate( delay( 0 ).then( ( ) =>
{
( < Array< number > >[ ] ).concat( index )
.forEach( index =>
{
this.deferrals[ index ].reject( error );
} );
} ) );
}

private ensureDeferral( index: number | ConcatArray< number > ): this
{
const indices = ( < Array< number > >[ ] )
.concat( index )
.sort( ( a, b ) => b - a );

const highest = indices[ 0 ];

for ( let i = this.deferrals.length; i <= highest; ++i )
this.deferrals.push( defer( void 0 ) );

return this;
}

private decorate( promise: Promise< void > )
: Promise< void > & this
{
// tslint:disable-next-line:variable-name
const This = {
decorate: this.decorate.bind( this ),
deferrals: this.deferrals,
ensureDeferral: this.ensureDeferral.bind( this ),
reject: this.reject.bind( this ),
resolve: this.resolve.bind( this ),
wait: this.wait.bind( this ),
} as unknown as this;

return Object.assign(
promise,
This
);
}
}

export function deferSet( )
{
return new OrderedAsynchrony( );
}
51 changes: 51 additions & 0 deletions test/defer-set.ts
@@ -0,0 +1,51 @@
import {
deferSet,
reflect,
} from "../";


describe( "deferSet", ( ) =>
{
it.concurrent( "resolve, wait", async ( ) =>
{
const order = deferSet( );

await order.resolve( 0 );
await order.wait( 0 );
} );

it.concurrent( "wait+resolve+reject all-in-one", async ( ) =>
{
const order = deferSet( );

await order.resolve( 0 );

await order.wait( 0, 1, 2 );

await order.wait( 1 );
const reflection = await reflect( order.wait( 2 ) );
expect( reflection.isRejected ).toBe( true );
expect( ( < Error >reflection.error ).message ).toMatch( /rejection/ );
} );

it.concurrent( "wait+resolve+reject all-in-one, arrays", async ( ) =>
{
const order = deferSet( );

await order.resolve( [ 0, 1 ] );

await order.wait( [ 0, 1 ], [ 2, 3 ], [ 4, 5 ] );

await order.wait( [ 2, 3 ] );
const reflection = await reflect( order.wait( [ 4, 5 ] ) );
expect( reflection.isRejected ).toBe( true );
expect( ( < Error >reflection.error ).message ).toMatch( /rejection/ );
} );

it.concurrent( "wait+resolve+reject all-in-one, empty arrays", async ( ) =>
{
const order = deferSet( );

await order.wait( [ ], [ ], [ ] );
} );
} );

0 comments on commit b1e27a1

Please sign in to comment.