@lightbend/akkaserverless-javascript-sdk
Version:
Akka Serverless JavaScript SDK
231 lines (206 loc) • 8.63 kB
JavaScript
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs');
const protobufHelper = require('./protobuf-helper');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const EventSourcedEntityServices = require('./event-sourced-entity-support');
const AkkaServerless = require('./akkaserverless');
const { GrpcUtil } = require('./grpc-util');
const eventSourcedEntityServices = new EventSourcedEntityServices();
/**
* An event sourced entity command handler.
*
* @callback module:akkaserverless.EventSourcedEntity~commandHandler
* @param {Object} command The command message, this will be of the type of the gRPC service call input type.
* @param {module:akkaserverless.Serializable} state The entity state.
* @param {module:akkaserverless.EventSourcedEntity.EventSourcedEntityCommandContext} context The command context.
* @returns {undefined|Object|module:akkaserverless.replies.Reply} The message to reply with, it must match the gRPC service call output type for this
* command, or if a Reply is returned, contain an object that matches the output type.
*/
/**
* An event sourced entity event handler.
*
* @callback module:akkaserverless.EventSourcedEntity~eventHandler
* @param {module:akkaserverless.Serializable} event The event.
* @param {module:akkaserverless.Serializable} state The entity state.
* @returns {module:akkaserverless.Serializable} The new entity state.
*/
/**
* An event sourced entity behavior.
*
* @typedef module:akkaserverless.EventSourcedEntity~behavior
* @property {Object<string, module:akkaserverless.EventSourcedEntity~commandHandler>} commandHandlers The command handlers.
*
* The names of the properties must match the names of the service calls specified in the gRPC descriptor for this
* event sourced entities service.
* @property {Object<string, module:akkaserverless.EventSourcedEntity~eventHandler>} eventHandlers The event handlers.
*
* The names of the properties must match the short names of the events.
*/
/**
* An event sourced entity behavior callback.
*
* This callback takes the current entity state, and returns a set of handlers to handle commands and events for it.
*
* @callback module:akkaserverless.EventSourcedEntity~behaviorCallback
* @param {module:akkaserverless.Serializable} state The entity state.
* @returns {module:akkaserverless.EventSourcedEntity~behavior} The new entity state.
*/
/**
* Initial state callback.
*
* This is invoked if the entity is started with no snapshot.
*
* @callback module:akkaserverless.EventSourcedEntity~initialCallback
* @param {string} entityId The entity id.
* @returns {module:akkaserverless.Serializable} The entity state.
*/
/**
* Options for an event sourced entity.
*
* @typedef module:akkaserverless.EventSourcedEntity~options
* @property {number} [snapshotEvery=100] A snapshot will be persisted every time this many events are emitted.
* It is strongly recommended to not disable snapshotting unless it is known that
* event sourced entities will never have more than 100 events (in which case
* the default will anyway not trigger any snapshots)
* @property {array<string>} [includeDirs=["."]] The directories to include when looking up imported protobuf files.
* @property {boolean} [serializeAllowPrimitives=false] Whether serialization of primitives should be supported when
* serializing events and snapshots.
* @property {boolean} [serializeFallbackToJson=false] Whether serialization should fallback to using JSON if an event
* or snapshot can't be serialized as a protobuf.
* @property {array<string>} [forwardHeaders=[]] request headers to be forwarded as metadata to the event sourced entity
* @property {module:akkaserverless.EventSourcedEntity~entityPassivationStrategy} [entityPassivationStrategy] Entity passivation strategy to use.
*/
/**
* Entity passivation strategy for an event sourced entity.
*
* @typedef module:akkaserverless.EventSourcedEntity~entityPassivationStrategy
* @property {number} [timeout] Passivation timeout (in milliseconds).
*/
/**
* An event sourced entity.
*
* @memberOf module:akkaserverless
* @implements module:akkaserverless.Entity
*/
class EventSourcedEntity {
/**
* Create a new event sourced entity.
*
* @constructs
* @param {string|string[]} desc A descriptor or list of descriptors to parse, containing the service to serve.
* @param {string} serviceName The fully qualified name of the service that provides this entities interface.
* @param {string} entityType The entity type name for all event source entities of this type. This will be prefixed
* onto the entityId when storing the events for this entity. Be aware that the
* chosen name must be stable through the entity lifecycle. Never change it after deploying
* a service that stored data of this type
* @param {module:akkaserverless.EventSourcedEntity~options=} options The options for this event sourced entity
*/
constructor(desc, serviceName, entityType, options) {
/**
* @type {module:akkaserverless.EventSourcedEntity~options}
*/
this.options = {
...{
entityType: entityType,
snapshotEvery: 100,
includeDirs: ['.'],
serializeAllowPrimitives: false,
serializeFallbackToJson: false,
},
...options,
};
if (!entityType) throw Error('EntityType must contain a name');
const allIncludeDirs = protobufHelper.moduleIncludeDirs.concat(
this.options.includeDirs,
);
this.root = protobufHelper.loadSync(desc, allIncludeDirs);
/**
* @type {string}
*/
this.serviceName = serviceName;
// Eagerly lookup the service to fail early
/**
* @type {protobuf.Service}
*/
this.service = this.root.lookupService(serviceName);
const packageDefinition = protoLoader.loadSync(desc, {
includeDirs: allIncludeDirs,
});
this.grpc = grpc.loadPackageDefinition(packageDefinition);
/**
* Access to gRPC clients (with promisified unary methods).
*
* @type module:akkaserverless.GrpcClientLookup
*/
this.clients = GrpcUtil.clientCreators(this.root, this.grpc);
}
/**
* @return {string} event sourced entity component type.
*/
componentType() {
return eventSourcedEntityServices.componentType();
}
/**
* Lookup a protobuf message type.
*
* This is provided as a convenience to lookup protobuf message types for use with events and snapshots.
*
* @param {string} messageType The fully qualified name of the type to lookup.
* @return {protobuf.Type} The protobuf message type.
*/
lookupType(messageType) {
return this.root.lookupType(messageType);
}
/**
* The initial state callback.
*
* @member module:akkaserverless.EventSourcedEntity#initial
* @type module:akkaserverless.EventSourcedEntity~initialCallback
*/
/**
* Set the initial state callback.
*
* @param {module:akkaserverless.EventSourcedEntity~initialCallback} callback The initial state callback.
* @return {module:akkaserverless.EventSourcedEntity} This entity.
*/
setInitial(callback) {
this.initial = callback;
return this;
}
/**
* The behavior callback.
*
* @member module:akkaserverless.EventSourcedEntity#behavior
* @type module:akkaserverless.EventSourcedEntity~behaviorCallback
*/
/**
* Set the behavior callback.
*
* @param {module:akkaserverless.EventSourcedEntity~behaviorCallback} callback The behavior callback.
* @return {module:akkaserverless.EventSourcedEntity} This entity.
*/
setBehavior(callback) {
this.behavior = callback;
return this;
}
register(allComponents) {
eventSourcedEntityServices.addService(this, allComponents);
return eventSourcedEntityServices;
}
}
module.exports = EventSourcedEntity;