JSPM

destructible

7.0.0-alpha.37
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 42
  • Score
    100M100P100Q80475F
  • License MIT

Controlled demolition of complex objects.

Package Exports

  • destructible

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (destructible) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Destructible

Actions Status codecov License: MIT

Controlled demoltion of async/await applications.

What Where
Discussion https://github.com/bigeasy/destructible/issues/1
Documentation https://bigeasy.github.io/destructible
Source https://github.com/bigeasy/destructible
Issues https://github.com/bigeasy/destructible/issues
CI https://travis-ci.org/bigeasy/destructible
Coverage: https://codecov.io/gh/bigeasy/destructible
License: MIT
npm install destructible

Destructible manages the concurrent asynchronous code paths in your application. At the very least, it provides the functionality of Promise.allSettled() but with dependencies, error handling and reporting, and cancellation.

This README.md is also a unit test using the Proof unit test framework. We'll use the Proof okay function to assert out statements in the readme. A Proof unit test generally looks like this.

require('proof')(4, async okay => {
    okay('always okay')
    okay(true, 'okay if true')
    okay(1, 1, 'okay if equal')
    okay({ value: 1 }, { value: 1 }, 'okay if deep strict equal')
})

You can run this unit test yourself.

git clone git@github.com:bigeasy/destructible.git
cd destructible
npm install --no-package-lock --no-save
make
node test/readme.t.js

The 'destructible' module exports a single Destructible object.

const Destructible = require('destructible')

Basic destructible usage.

{
    const destructible = new Destructible('example')

    const work = [ 1, 2, 3, 4 ].map(work => Promise.resolve(work))

    let sum = 0
    destructible.ephemeral('loop', async () => {
        while (work.length != 0) {
            sum += await work.shift()
        }
    })

    await destructible.destroy().promise

    okay(sum, 10, 'basic destructible')
}

Destructing With Errors

Destructible does not provide any sort of error recovery mechanism, it assumes that you will perform any error recovery in your appliction strands using try/catch or what-have-you. If a strand rejects, the Destructible will be destroyed and destruction will begin.

There is however an error notification mechanism so that your services can detect when the failure of other services so that they don't attempt to perform an orderly shutdown when the services they depend upon are in an unstable state.

If you have a database service fails a single write for some reason, it should probably not perform its ordinary database shutdown procedure when stacks are unwinding and the program is headed for oblivion. It would be better to perform a recovery procedure after the system administrator investigates the fault and restarts the program.

You can determine if a Destructible exited with an error using the errored property. This is a synchronous property that will return false until the Destructible destructs, then it will return return true if the Destruction was due to an error.

This property exists on all the Destructibles in the destructible tree. If any fail then all destructibles will have their errored property set, so the errored property is really a property of the destructible tree, not of any individual destructible itself.

const destructible = new Destructible('destructible')

const child = destructible.durable('child')
const sibling = destructible.durable('sibling')

child.durable('errored', async () => { throw new Error('reject') })

try {
    await destructible.promise
} catch (error) {
    okay(child.errored, 'child errored')
    okay(sibling.errored, 'sibling errored')
    okay(destructible.errored, 'parent errored')
    okay(child.errored && sibling.errored && destructible.errored, 'everyone errored')
}

An error in one service is not always an error in every service. Using our database service example, an error in the database service means that all other services should stop work since their work cannot be saved. But an error in a service that depends on the database service shouldn't stop the database service from saving the writes it has queued and shutting down in an orderly fashion.

We can isolate errors in sub-trees of the destructible tree using the isolated property when we create a sub-destructible. When an error occurs outside of the isolated sub-tree, the destructibles in the isolated sub-tree will not have their errored property set.

const destructible = new Destructible('top')

const outside = destructible.durable('outside')
const group = destructible.durable({ isolated: true }, 'group')
const sibling = group.durable('sibling')
outside.durable('errored', async () => { throw new Error('error') })
try {
    await destructible.promise
} catch (error) {
    okay(destructible.errored, 'root errored')
    okay(!group.errored, 'group errored')
    okay(!group.sibling, 'sibling errored')
    okay(outside.errored, 'outside errored')
}

When an error occurs inside of the isolated sub-tree, the destructibles outside the isolated sub-tree will have their errored property set.

const destructible = new Destructible('top')

const outside = destructible.durable('outside')
const group = destructible.durable({ isolated: true }, 'group')
const sibling = group.durable('sibling')
group.durable('errored', async () => { throw new Error('error') })
try {
    await destructible.promise
} catch (error) {
    okay(destructible.errored, 'root errored')
    okay(sibling.errored, 'sibling errored')
    okay(outside.errored, 'outside errored')
}

Error can occur after destruction.

At destruction a service might wait for another service to drain but will skip the drain if the destructible tree is errored because the drain might never come. If the destructible tree enters an errored state after destruction, this service has already begin waiting. It needs a way to be notified so it can cancel its wait on drain.

For this notification we can register a panic handler using panic(). The panic handler must be synchronous. It can launch new ephemerals, but I doubt that's a good idea. If I do so myself I'll come back and talk about the use case.

The panic handler is supposed in indicate that an error occurred after shutdown, not before. The panic() handler will only be called after destruct() and only if the the destructible was not already errored when it was destructed.

In this example the shutdown of a consumer is expecting an a producer to signal it is done by releasing a drain latch. Before the producer can release the latch, however, it raises an exception. The consumer registered a panic handler that will release the latch itself so that the consumer shutdown will complete.

const destructible = new Destructible('destructible')

const test = []

const drain = function () {
    let capture
    return {
        promise: new Promise(resolve => capture = { resolve }),
        ...capture
    }
} ()

const producer = destructible.durable('producer')
producer.destruct(() => test.push(`producer errored: ${producer.errored}`))
producer.panic(() => test.push('producer panicked'))

const consumer = destructible.ephemeral('consumer')
consumer.destruct(() => {
    test.push(`consumer errored: ${consumer.errored}`)
    consumer.ephemeral('shutdown', async () => {
        await drain.promise
    })
})
consumer.panic(() => {
    test.push('consumer panicked')
    drain.resolve()
})
consumer.destroy()

producer.durable('rejected', async () => {
    throw new Error('reject')
    drain.resolve()
})

try {
    await destructible.promise
} catch (error) {
    okay(test, [
        'consumer errored: false', 'consumer panicked', 'producer errored: true'
    ], 'panic')
}

Panic handlers will not run if the destructible resolves or scrams.

In this example we wait for the sibling to shutdown completely before the child raises an exception. Because the sibling shutdown completely, it's panic handler is not called.

const destructible = new Destructible('destructible')

const panic = []

const child = destructible.durable('child')
child.panic(() => panic.push('child panicked'))

const sibling = destructible.ephemeral('sibling')
sibling.panic(() => panic.push('sibling panicked'))
sibling.destroy()

await sibling.promise

child.durable('rejected', async () => { throw new Error('reject') })

try {
    await destructible.promise
} catch (error) {
    okay(panic, [], 'no panic')
}