@debonet/es6pacts
Version:
Releasable/cancellable and Reporting Promises for Javascript ES6
165 lines (114 loc) • 7.57 kB
Markdown
# es6pacts
Releasable, reporting Promises for JavaScript ES6.
## TL;DR
* A Pact is a Promise that can be released from its obligation (`release`, `resolve`, `reject`, `fulfill`) and that delivers progress reports to independent observers (`.progress`).
* One line of composition: `fclassReleasable( Task )`. Release semantics come from `@debonet/es6pledges`; reporting semantics come from `@debonet/es6tasks`.
* The constructor takes a single executor argument, `( fResolve, fReject, fReport ) => xPolicy`. The executor's RETURN VALUE is the release policy; the v2 second constructor argument is removed.
* Progress reports stop when the pact settles. This is a library guarantee (post-settle suppression), not executor discipline.
## Example
```javascript
const Pact = require( "@debonet/es6pacts" );
function fpactDelay( dtm, dtmTick = 100 ){
return new Pact(( fResolve, fReject, fReport ) => {
let dtmElapsed = 0;
const interval = setInterval(() => {
dtmElapsed += dtmTick;
fReport( dtmElapsed );
if ( dtmElapsed >= dtm ){
clearInterval( interval );
fResolve( dtm );
}
}, dtmTick );
return () => clearInterval( interval );
});
}
```
Hold the SOURCE pact and release it; cancellation flows downstream as ordinary settlement:
```javascript
const pactDelay = fpactDelay( 10000 );
pactDelay
.progress(( dtmElapsed ) => console.log( "progress:", dtmElapsed ))
.then(( x ) => console.log( "result:", x ));
setTimeout(() => { pactDelay.resolve( "faster" ); }, 250 );
```
Output:
```
progress: 100
progress: 200
result: faster
```
* The release at 250ms lands mid-gap between the 200ms and 300ms ticks, so the output is stable.
* The cleanup is the executor's return value: `return () => clearInterval( interval );`. It clears the interval, and post-settle suppression guarantees no report after settlement.
* The usage releases the source pact, not a chain tail. Releasing a derived pact settles only that pact and skips its own handler (see Chains).
Same example, three libraries: remove the `fReport` executor parameter and the `fReport( dtmElapsed );` line and this is `fpledgeDelay` (`@debonet/es6pledges` — releasable, no reporting). Remove instead the `return () => clearInterval( interval );` line and this is `ftaskDelay` (`@debonet/es6tasks` — reporting, not releasable). All other lines are identical across the three libraries. Pact is the union, exactly as `fclassReleasable( Task )` is the union of the two libraries.
## Chain Windowing
Using the same `fpactDelay`, attachment position selects what a `.progress` observer sees:
```javascript
const pactFirst = fpactDelay( 10000 );
const pactDone = pactFirst
.progress(( dtm ) => console.log( "first stage:", dtm ))
.then(( x ) => fpactDelay( 300 ))
.progress(( dtm ) => console.log( "whole chain:", dtm ));
setTimeout(() => pactFirst.resolve( "faster" ), 350 );
pactDone.then(( x ) => console.log( "result:", x ));
```
Output:
```
first stage: 100
whole chain: 100
first stage: 200
whole chain: 200
first stage: 300
whole chain: 300
whole chain: 100
whole chain: 200
whole chain: 300
result: 300
```
What the example shows:
* The `.progress` attached BEFORE the `.then` observes only `pactFirst`'s stage.
* The `.progress` attached AFTER the `.then` observes the forwarded parent stream plus the stream of the pact the handler returns.
* The release arrives mid-chain: `pactFirst.resolve( "faster" )` settles the first stage, its reports stop (post-settle suppression), and the early value flows downstream as ordinary settlement.
## The Executor
`new Pact( fxExecutor )` takes ONE argument. Task supplies `fReport` as the executor's third parameter, so the full signature is `( fResolve, fReject, fReport ) => xPolicy`. The return value `xPolicy` is the release policy:
* A function `( xReason ) => ...` — the release handler. It closes over `fResolve`, `fReject`, `fReport`, and the executor's locals (the useEffect-cleanup idiom).
* An Error instance — unconditional refusal: every `release()` rejects with it.
* `undefined` or anything else — releasable with no cleanup.
## Release
`pact.release( xReason )` runs a consent pipeline: the policy may clean up and consent, or refuse. On consent the pact settles by type dispatch on `xReason`:
* `PactResolve` → resolves with its cause
* `PactReject` → rejects with its cause
* any other Error → rejects with it
* anything else → resolves with it
`Pact.PactResolve` and `Pact.PactReject` are statics on the class.
The `release()` return contract:
* FULFILLS with a settlement record `{ sStatus, x }` after the pact settles, ordered after handlers registered before the call.
* REJECTS only on refusal (the release handler threw or rejected) while the pact was still pending. A refused pact stays pending, keeps all handlers, and keeps reporting.
Settlement is a report that the obligation ended; nothing settles the pact without the release handler's consent. No forced settlement exists.
## Settle Verbs
* `pact.resolve( x )` ≡ `pact.release( new PactResolve( x ))`
* `pact.reject( e )` ≡ `pact.release( new PactReject( e ))`
* `pact.fulfill( x )` — alias of `resolve( x )`
All refusable, same pipeline. The instance `resolve` / `reject` are distinct from the inherited statics `Pact.resolve` / `Pact.reject`, which construct already-settled pacts.
## releaseOn
`pact.releaseOn( signal )` requests a release when the AbortSignal fires, with `signal.reason` as `xReason`; it returns `this`. A bare `abort()` produces an AbortError, which rejects by dispatch; `abort( xValue )` resolves with the value. This is a refusable request, not an unconditional deadline.
## Chains
Cancel a pipeline at its SOURCE: release the base pact you hold, and cancellation flows downstream as ordinary settlement. Releasing a derived pact never affects an ancestor; it settles the derived pact (and at most one owned child: an in-flight pact returned by its own stage handler).
## Reporting
* `pact.progress( f )` registers an independent observer and returns `this` (chainable).
* Every observer receives the SAME raw value passed to `fReport`, in attachment order. Observer return values are ignored.
* Delivery is queued on a microtask. There is no replay buffer: attach late, miss earlier reports.
* Once the pact settles, further reports are dropped by the library.
* Derived pacts forward their parent's report stream, plus the stream of any pact a handler returns. Attachment position selects the observation window (see Chain Windowing).
* Combinators (`Pact.all`, `any`, `race`, `allSettled`) re-emit member reports as `{ task : n, report : x }` and suppress them once the combined pact settles. Releasing a combined pact settles the combined pact only; members are not owned and keep running.
## Interlocking Guarantees
* A released pact is a settled pact, so its reports stop — suppression is central, in the library.
* A refused release leaves the pact pending, so reports keep flowing during and after the refusal.
## Full Semantics
This module adds no behavior of its own; the complete contracts live upstream:
* `@debonet/es6pledges` `specs/v3-core.md` — constructor, executor, policy, pipeline, dispatch, wrappers, release contract, verbs, `releaseOn`.
* `@debonet/es6pledges` `specs/v3-chains-combinators.md` — chain and combinator release.
* `@debonet/es6tasks` `specs/v3-reporting.md` — observer model, microtask delivery, post-settle suppression, windowing, combinator report format.
## See Also
* @debonet/es6pledges
* @debonet/es6tasks