Lifecycle

The Lifecycle component is the foundation of the Hashgraph DID SDK. Tt provides a powerful and flexible system for orchestrating complex asynchronous operations related to Decentralized Identifiers (DIDs).

Overview

The Lifecycle Management component simplifies the development of robust and reliable DID-related applications by streamlining the process of defining, managing, and executing sequences of asynchronous steps. It provides features such as:

  • Step-by-Step Execution: Define a clear sequence of asynchronous steps for DID operations.

  • Callback Integration: Incorporate custom callback functions at any point in the lifecycle.

  • Signature Handling: Include signature generation and verification steps seamlessly.

  • Pause/Resume Functionality: Introduce pauses for manual intervention or external interactions, and resume execution when ready.

  • Error Handling: Implement robust error handling with catch steps to gracefully handle exceptions.

  • Flexible Builder Pattern: Provides a fluent API for building and configuring lifecycles with ease.

These features enable developers to create complex DID workflows while maintaining a clear and organized structure.

Using the Lifecycle Management Component

The core of the Lifecycle Management component is the LifecycleBuilder class. It provides a fluent API for defining the steps involved in a DID operation. Here’s a basic example of how to use the LifecycleBuilder:

import { LifecycleBuilder, LifecycleRunner } from "@hashgraph-did-js-sdk/lifecycle";

const lifecycle = new LifecycleBuilder()
  .callback('label1', async (message: Message, publisher: Publisher) => {
    const topicId = await fetchTopicId();
    message.setTopicId(topicId);
  })
  .signWithSigner('label2') // Sign the message
  .callback('label3', async (message: Message, publisher: Publisher) => {
    const payload = message.getPayload();
    await publisher.submit(payload);
  })
  .catch('label3', (error) => {
    // Handle errors gracefully
    console.error("Lifecycle error:", error);
  });

const runner = new LifecycleRunner(lifecycle);
const state = await runner.process(message, {
  // Provide necessary context (e.g., signer, publisher)
  publisher,
  signer,
});

This example demonstrates a simple lifecycle with three steps:

  1. An asynchronous callback function that fetches some data and sets it in the message.

  2. A signing step using signWithSigner.

  3. Another asynchronous callback function that submits the data using a publisher.

The catch method is used to handle any errors that occur during the lifecycle execution.

The LifecycleRunner class is responsible for executing the lifecycle. The process method starts the execution and returns a RunnerState object, which contains information about the current state of the lifecycle.

Pausing Execution

You can introduce pauses in the lifecycle to allow for manual intervention or waiting for external events. Here’s an example of how to add a pause step:

const lifecycle = new LifecycleBuilder()
  .callback('label1', async (message: Message, publisher: Publisher) => {
    // Perform some asynchronous operation
  })
  .pause('pause1') // Pause execution
  .callback('label2', async (message: Message, publisher: Publisher) => {
    // Perform another operation after the pause
  });

const runner = new LifecycleRunner(lifecycle);
const state = await runner.process(message, {
  publisher,
  signer,
});

// Do something before resuming execution

// Resume execution
const finalState = await runner.resume(state, {
  publisher,
  signer,
});

In this example, the pause method is used to introduce a pause in the lifecycle. The process method starts the execution, and the resume method resumes the execution after the pause. The resume method requires the RunnerState object returned by the process method and the necessary context to continue the execution (eg. publisher, signer).

Providing signature

You can add a signature step to the lifecycle to sign the message by yourself. Signature must be provided as an option in lifecycle runner. Here’s an example of how to add a signature step:

const lifecycle = new LifecycleBuilder()
  .callback('label1', async (message: Message, publisher: Publisher) => {
    // Perform some asynchronous operation
  })
  .pause('pause1') // Pause execution
  .signature('signature1') // Add signature to the message;

const runner = new LifecycleRunner(lifecycle);
const state = await runner.process(message, {
  publisher,
});

// Extract bytes from the message and sign it

// Resume execution with signature
const finalState = await runner.resume(state, {
  publisher,
  args: {
    signature: signatureBytes,
  },
});

The signature is provided as an option in the resume method. The args object contains the signature bytes that will be used to sign the message. In this case a Signer class is not required.

Error Handling

The catch method is used to handle errors that occur during the lifecycle execution. You can define custom error handling logic to gracefully handle exceptions. Here’s an example of how to use the catch method:

const lifecycle = new LifecycleBuilder()
  .callback('label1', async (message: Message, publisher: Publisher) => {
    throw new Error("Something went wrong");
  })
  .catch('error-handler', (error) => {
    // Handle errors gracefully
    console.error("Lifecycle error:", error);
  });

const runner = new LifecycleRunner(lifecycle);
const state = await runner.process(message, {
  publisher,
  signer,
});