Implementing Value Entities in JavaScript

Value Entities persist their state on every change, and Akka Serverless needs to serialize their data to send it to the underlying data store. The most straight forward way to persist the state is to use protobuf types. Akka Serverless will automatically detect if an updated state is protobuf, and serialize it using protobufjs. See https://www.npmjs.com/package/protobufjs for more information on protobufjs. For other serialization options, including JSON, see Serialization.

While protobufs are the recommended format for persisting state, we recommend that you do not persist your service’s protobuf messages. While this may introduce some overhead to convert from one type to the other, it will allow the service’s public interface to evolve independent of the data storage format, which should be private.

Defining the proto files

Using a shopping cart example, application domain objects are defined in a file named domain.proto. It defines Entity state in the message Cart, which contains a collection of LineItems.

syntax = "proto3";

package example.shoppingcart.domain;

message LineItem {
    string productId = 1;
    string name = 2;
    int32 quantity = 3;
}

message Cart {
    repeated LineItem items = 1;
}

The shoppingcart proto file defines the service API:

syntax = "proto3";

import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto";
import "akkaserverless/eventing.proto";
import "google/api/annotations.proto";
import "google/api/http.proto";
import "google/api/httpbody.proto";

package example.shoppingcart;

message AddLineItem {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
    string product_id = 2;
    string name = 3;
    int32 quantity = 4;
}

message RemoveLineItem {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
    string product_id = 2;
}

message GetShoppingCart {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
}

message RemoveShoppingCart {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
}

message LineItem {
    string product_id = 1;
    string name = 2;
    int32 quantity = 3;
}

message Cart {
    repeated LineItem items = 1;
}

service ShoppingCart {
    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
        option (google.api.http) = {
            post: "/ve/cart/{user_id}/items/add"
            body: "*"
        };
    }

    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/ve/cart/{user_id}/items/{product_id}/remove";
    }

    rpc GetCart(GetShoppingCart) returns (Cart) {
        option (google.api.http) = {
            get: "/ve/carts/{user_id}"
            additional_bindings: {
                get: "/ve/carts/{user_id}/items"
                response_body: "items"
            }
        };
    }

    rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/ve/carts/{user_id}/remove";
    }
}

Creating an Entity

The following code creates the Value Entity with the akkaserverless.ValueEntity class. It passes in:

  • The protobuf files that define the service and the domain protocol, shoppingcart.proto and domain.proto. Akka Serverless uses the service protobuf file to load and read the service. It needs the protobuf file that defines domain state to know how to serialize state it receives from the proxy.

  • The fully qualified name of the service our Value Entity implements, example.shoppingcart.ShoppingCartService. The entityType is used to namespace the state in the journal.

const ValueEntity = require("@lightbend/akkaserverless-javascript-sdk").ValueEntity;

const entity = new ValueEntity(
  ["shoppingcart.proto", "domain.proto"],
  "example.shoppingcart.ShoppingCartService",
  "shopping-cart"
);

Using protobuf types

When you pass the state to Akka Serverless to persist, it needs to know how to serialize it. Simply passing a regular object does not provide enough information. Hence, any state types that you want to use, you have to first lookup the protobuf type, and then use the create method to create it.

The ValueEntity class provides a helper method called lookupType to facilitate this. So before implementing anything, we’ll look up these types so that we can use them later.

const Cart = entity.lookupType("example.shoppingcart.Cart");

Initial state

An Entity needs to have an initial state when it is first created and no state has been persisted for it yet. Value Entities are not explicitly created, they are implicitly created when a command arrives for them. Nothing is persisted on creation. So, if user "X" opens their shopping cart for the first time, an entity will be created, but it will have nothing stored yet, and just be in the initial state.

To create the initial state, set the initial callback. This takes the Id of the Entity being created, and returns a new empty state, in this case, an empty shopping cart:

entity.setInitial(userId => Cart.create({items: []}));

Note the use of Cart.create(), this creates a protobuf message using the Cart protobuf message type that code shown earlier looked up.

Behavior

Value Entity behavior is defined in command handlers. A command handler is a function that takes a command, the current state, and an ValueEntityCommandContext. It implements a service call on the entities gRPC interface.

The command is the input message type for the gRPC service call. For example, the GetCart service call has an input type of GetShoppingCart, while the AddItem service call has an input type of AddLineItem. The command will be an object that matches the structure of these protobuf types.

The command handler must return a message of the same type as the output type of the gRPC service call, in the case of our GetCart command, this must be a Cart message. Note that unlike for the state, this message does not need to be created using a looked up protobuf message type—​Akka Serverless already knows the output type of the gRPC service call and so can infer it itself. It only has to be a plain JavaScript object that matches the structure of the protobuf type.

Retrieving state

The following example shows the implementation of a GetCart command handler. This command handler is a read-only command handler—​it doesn’t update the state, it just returns it:

function getCart(request, cart, ctx) {
  return cart;
}

Updating state

When updating the state, a command handler MUST persist that change by calling updateState method on the ValueEntityCommandContext. If it does not, any change to the state will be lost when the next command arrives.

The following command handler updates the state. It also validates the command, ensuring the quantity items added is greater than zero. Invoking fail fails the command—​this method throws so there’s no need to explicitly throw an exception.

function addItem(addItem, cart, ctx) {
  // Validation:
  // Make sure that it is not possible to add negative quantities
  if (addItem.quantity < 1) {
    ctx.fail("Cannot add negative quantity to item " + addItem.productId);
  } else {
    // If there is an existing item with that product id, we need to increment its quantity.
    const existing = cart.items.find(item => {
      return item.productId === addItem.productId;
    });

    if (existing) {
      existing.quantity = existing.quantity + addItem.quantity;
    } else {
      // Otherwise, we just add the item to the existing list.
      cart.items.push(addItem);
    }
    ctx.updateState(cart);
  }
  return {};
}

Starting the entity

If you only have a single entity, as a convenience, you can start it directly, by invoking the start method, like so:

    entity.start();

Alternatively, you can add it to the AkkaServerless server explicitly using addComponent.

    const AkkaServerless = require("@lightbend/akkaserverless-javascript-sdk").AkkaServerless;
    const server = new AkkaServerless();
    server.addComponent(entity);