npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

immutable-core-task

v0.1.12

Published

Multi-step tasks based on Immutable Core

Downloads

42

Readme

immutable-core-task

Immutable Core Task integrates with the Immutable App ecosystem to provide a mechanism for defining multi-step tasks that run outside the context of a single process.

Defining a task

const orderTask = new ImmutableCoreTask({
    data: {
        foo: true,
    },
    methods: {
        capturePayment: capturePayment,
        checkDeliveryStatus: checkDeliveryStatus,
        checkInventoryAvailability: checkInventoryAvailability,
        fulfilOrder: fulfilOrder,
        refundPayment: refundPayment,
    },
    name: 'order',
    steps: [
        {
            method: 'module.email.orderReceived',
        },
        {
            error: 'module.email.inventoryUnavailable',
            method: 'checkInventoryAvailability',
        },
        {
            error: 'module.email.verifyPaymentDetails',
            method: 'capturePayment',
            reverse: 'refundPayment'
        },
        {
            error: 'module.email.orderCanceled',
            method: 'fulfilOrder',
        },
        {
            method: 'module.email.orderShipped',
        },
        {
            error: 'task.deliveryException.new',
            method: 'checkDeliveryStatus',
        },
        {
            method: 'module.email.orderDelivered',
        },
        {
            method: 'completeOrder',
        }
    ],
})

In this example a simple order processing flow is mocked out.

Every task must have a name which will be used to create an Immutable Core Module with the name ${name}Task (e.g. fooTask).

A task may have default data specified. The args passed when creating a new task instance will be merged over this data.

Every task must have one or more steps.

Each step in the flow will be executed in order.

Each step must have a method which is the code that will be executed for the step.

The method can be a reference to a method defined on the task itself like authorizeCreditCard or it can be a reference to a method on another Immutable Core module like module.email.orderReceived.

A task step can also initiate another task like task.deliveryException.new.

The error method specified for a step is called if the method for a step has an error.

If an error on any step occurs then any reverse methods on completed steps prior to the error will be executed in reverse order.

Tasks vs. transactions

In a traditional database a transaction is used to insure that multiple data modifications are all committed at the same time.

Tasks are similar to transactions in that they are designed to provide reliable completion of multi-step tasks and defined behaviors when errors occur.

A major difference between a task and a transaction is that transactions only work within the context of a single database.

Tasks are designed to facilitate multi-step processes that span multiple databases and multiple systems both internal and external.

Unlike transactions every step in a task will be committed as soon as it is complete. If a completed step needs to be reversed due to an error on a later step the reverse method must be explicitly defined.

Creating a new task instance

await ImmutableCoreTask.task('order').new({...})

To execute a task the new method is called. The arguments to new provide the initial context for the task.

The session that is passed to new will be the session that is used for executing all of the methods in the tasks steps so the access control for those methods and anything they do will be based on the session that the task was initialized with.

await this.task.order.new({...})

From inside a controller or any other Immutable Core Module the Immutable AI instance bound to this is the best way to create a new task.

Task execution

When a new task instance is created the task instance will be saved using the Immutable Core Model task.

The task model, the task runner, and task administration are all defined in the Immutable App Task module.

As soon as the task record is saved the new task instance will be returned.

Once the task is saved it will be picked up and executed by the next available task runner.

Task step execution

Whenever a task is saved a nextRunTime value will be set. When a new task instance is created the nextRunTime will be the current time unless it is specifically scheduled for the future.

Task runners will process tasks in order of their nextRunTime once that time is reached.

When a task runner selects a task to execute it will update the task data with the step that it intends to execute and the information about the task runner and then set a new nextRunTime based on a timeout value.

If the task runner completes the step, with either a success or failure, it will update the task record with the result.

If there is another step that can be executed immediately then the same task runner will execute the next step.

If there is another step that is scheduled to execute in the future then the task runner will move on to other tasks.

If the task runner does not complete the execution of the step before the timeout and the nextRunTime is reached then another task runner will pick up the task and attempt to complete it.

Error handling

Task ignoreError

const orderTask = new ImmutableCoreTask({
    ignoreError: true
})

When ignoreError is set to true then tasks will continue to process and will be completed successfully even if one or more steps fail.

This option is false by default.

Setting ignoreError true for the task means that errors on any step will not prevent the task from completing successfully unless ignoreError is set to false for specific steps.

Step ignoreError

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            ignoreError: true,
            method: 'module.email.orderReceived',
            retry: true,
        },
    ],
})

Any ignoreError option set at the step level will override the task level configuration.

When ignoreError is used with retry processing of later steps will not continue until after all retry attempts have been made.

Retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            method: 'module.email.orderReceived',
            retry: true,
        },
    ],
})

In many cases, especially where a step involves a network request, it is desirable to retry the step if an error occurs.

When retry is set to true the task runner will retry the step at least once and may retry the step multiple times based on the type of error.

If a step has an error method and retry is enabled the error method will only be called if all retry attempts fail.

retry can also be specified for check, error and reverse methods.

Retry all

const orderTask = new ImmutableCoreTask({
    retry: true,
    steps: [
        {
            method: 'module.email.orderReceived',
        },
    ],
})

When retry is set to true in the top level task args all methods will have retry set to true.

Check

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            check: 'module.email.wasOrderReceivedSent',
            retry: true,
        },
    ],
})

It is possible that after a step completes successfully an error occurs that prevents that success from being recorded in the database.

A check method can be specified that checks if the step has already been completed.

The check method will be called with the same arguments that the step method was called with unless a custom input map is specified.

If the check method resolves with a value then the step will be marked as complete without retrying it again.

If check returns a value that value will be only be merged into the task data if an output map is defined.

If the check method encounters an error then the step enters an error state, the method will not be retried, and the error method will be called if it is defined.

The check method can only be called when doing a retry so retry must be enabled for a check method to be defined.

Check retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            check: {
                method: 'module.email.wasOrderReceivedSent',
                retry: true,
            },
            retry: true,
        },
    ],
})

Error

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            error: 'module.email.orderCanceled',
            retry: true,
        },
    ],
})

The error method specifies the action to take if the step method encounters an error.

If retry is enabled then the error method will only be called once after all retry attempts have failed.

If retry is not enabled then the error method will be called immediately.

If the error method itself encounters an error then the task enters an error state and it will be marked as failed unless the ignoreError option is specified.

The error method will be called with the same args as the step method unless an input map is specified. The error will always be added to the args.

If error returns a value that value will be only be merged into the task data if an output map is defined.

Error retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            error: {
                method: 'module.email.orderCanceled',
                retry: true,
            },
        },
    ],
})

Error retry with check

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            error: {
                check: 'module.email.wasOrderCanceledEmailSent',
                method: 'module.email.orderCanceled',
                retry: true,
            },
        },
    ],
})

If retry is enabled then the error method can have its own check method that works exactly the same as the step check method.

The check method will be called with custom arguments if it has an input map defined. Otherwise it will fall back to the error or method arguments.

If check returns a value that value will be only be merged into the task data if an output map is defined.

Error retry with check retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            error: {
                check: {
                    method: 'module.email.wasOrderCanceledEmailSent',
                    retry: true,
                },
                method: 'module.email.orderCanceled',
                retry: true,
            },
        },
    ],
})

Reverse

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            reverse: 'refundPayment',
        },
    ],
})

The reverse method will be called if the step it is defined for completed successfully but a later step encountered an error.

The reverse method will be called with the same arguments as the step method unless an input map is specified.

If reverse returns a value that value will be only be merged into the task data if an output map is defined.

Reverse retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            reverse: {
                method: 'refundPayment',
                retry: true,
            }
        },
    ],
})

Reverse retry with check

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            reverse: {
                check: 'wasPaymendRefunded',
                method: 'refundPayment',
                retry: true,
            }
        },
    ],
})

If retry is enabled then the reverse method can have its own check method that works exactly the same as the step check method.

The check method will be called with custom arguments if it has an input map defined. Otherwise it will fall back to the reverse or method arguments.

If check returns a value that value will be only be merged into the task data if an output map is defined.

Reverse retry with check retry

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            reverse: {
                check: {
                    method: 'wasPaymendRefunded',
                    retry: true
                },
                method: 'refundPayment',
                retry: true,
            }
        },
    ],
})

Method call args

By default every step method will be called with the task data which includes the session that the task instance was created with.

Any data that is returned by the method will be merged into the task data.

Setting specific method args

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            input: {
                foo: 'bar',
                'foo.bam': 'baz.bam',
            },
            method: 'module.email.orderReceived',
        },
    ],
})

When integrating internal and external systems it will often be necesary to perform a mapping between different data properties.

The input object will be used to get and set the properties that are used for each method call.

The key property in the input map (e.g. foo, foo.bam) is used to lookup the value from the task value and the value in the input map is used to set the value for the method args.

input: {dataProperty: 'argsProperty'}

Properties are resolved using lodash _.get and _.set so deeply nested values can be retrieved.

The session is always included with every method call so it does not need to be explicitly specified.

Setting specific method return data

const orderTask = new ImmutableCoreTask({
    steps: [
        {
            method: 'module.email.orderReceived',
            output: {
                bar: 'foo',
                'baz.bam': 'foo.bam',
            },
        },
    ],
})

The output map works similarly to the input map with the major difference being that the key property is for the return data and the value property is for the task data.

output: {returnProperty: 'dataProperty'}

If output is defined then only the properties in the output map will be merged into the task data.

Task runner

var task = new ImmutableCoreTaskInstance({record: record})

await taskInstance.run()

The task runner loads task instance records where the nextRunTime is less than or equal to the current time and then instantiates an ImmutableCoreTaskInstance from the record.

The task runner then calls run which will run as many steps as possible until the task either finishes or hits a break, such as waiting to retry after an error.

Task schema

{
    data: {},
    name: 'taskName',
    steps: [
        { ... },
    ],
}

ImmutableCoreTasks are persisted using the ImmutableCoreModel specified by taskModel.

The sync method is used to make sure the current task definition is persisted to the database.

When a task instance is executed the taskId will be used to fetch the version of the task that the instance was created with.

If the steps in a task are changed all current task instances will run the task steps that they were created with.

Task instance schema

{
    data: {},
    nextRunTime: '2001-01-01 01:01:01',
    status: {
        complete: false,
        running: false,
        step: 0,
        success: false,
    },
    taskId: '<hex task id>',
    taskName: 'foo',
}

When a new task instance is created it will be persisted using the ImmutableCoreModel specified by instanceModel.

The status object is used to track the status of task instance execution.

The status object changes from one execution run to the next and not all properties will always be present but the complete history of the task instance execution can always be retrieved by looking at the history of the instance revisions.

nextRunTime will either have the next scheduled run time for execution of the task or will be NULL if task execution has completed.

Status properties

| name | type | description | |---------------|-----------|--------------------------------------------------| | complete | boolean | set true when task instance execution complete | | error | object | record of sub-step errors | | retry | boolean | set true when next step should be retry of last | | reverse | boolean | set to true if running reverse method(s) | | running | boolean | true when step is being executed | | subStep | string | sub-step name: method, check, error, etc | | step | object | specification of current step | | stepNum | integer | number of current step - starting at zero | | success | boolean | set true if complete without unhandled errors | | successOrig | boolean | original success value before running reverse | | try | object | record of try counts for sub-steps |

Running first step of task

{
    running: true,
    step: 0,
}

The run method will update the status of the task instance to running: true and update nextRunTime to the timeout value for the step.