UNPKG

@lightbend/akkaserverless-javascript-sdk

Version:
310 lines 12.7 kB
"use strict"; /* * 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 tslib_1 = require("tslib"); const path = require('path'); const debug = require('debug')('akkaserverless-replicated-entity'); const util = require('util'); const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); const protoHelper = require('./protobuf-helper'); const AnySupport = require('./protobuf-any'); const replicatedData = require('./replicated-data'); const CommandHelper = require('./command-helper'); const { Metadata } = require('./metadata'); class ReplicatedEntityServices { constructor() { this.services = {}; this.includeDirs = [ path.join(__dirname, '..', 'proto'), path.join(__dirname, '..', 'protoc', 'include'), path.join(__dirname, '..', '..', 'proto'), path.join(__dirname, '..', '..', 'protoc', 'include'), ]; } addService(entity, allComponents) { this.services[entity.serviceName] = new ReplicatedEntitySupport(entity.root, entity.service, { commandHandlers: entity.commandHandlers, onStateSet: entity.onStateSet, defaultValue: entity.defaultValue, }, allComponents); } componentType() { return 'akkaserverless.component.replicatedentity.ReplicatedEntities'; } register(server) { const packageDefinition = protoLoader.loadSync(path.join('akkaserverless', 'component', 'replicatedentity', 'replicated_entity.proto'), { includeDirs: this.includeDirs, }); const grpcDescriptor = grpc.loadPackageDefinition(packageDefinition); const entityService = grpcDescriptor.akkaserverless.component.replicatedentity .ReplicatedEntities.service; server.addService(entityService, { handle: this.handle.bind(this), }); } handle(call) { let service; call.on('data', (replicatedEntityStreamIn) => { // cycle through the ReplicatedEntityStreamIn type, this will ensure default values are initialised replicatedEntityStreamIn = protoHelper.moduleRoot.akkaserverless.component.replicatedentity.ReplicatedEntityStreamIn.fromObject(replicatedEntityStreamIn); if (replicatedEntityStreamIn.init) { if (service != null) { service.streamDebug('Terminating entity due to duplicate init message.'); console.error('Terminating entity due to duplicate init message.'); call.write({ failure: { description: 'Init message received twice.', }, }); call.end(); } else if (replicatedEntityStreamIn.init.serviceName in this.services) { service = this.services[replicatedEntityStreamIn.init.serviceName].create(call, replicatedEntityStreamIn.init); } else { console.error("Received command for unknown Replicated Entity service: '%s'", replicatedEntityStreamIn.init.serviceName); call.write({ failure: { description: "Replicated Entity service '" + replicatedEntityStreamIn.init.serviceName + "' unknown.", }, }); call.end(); } } else if (service != null) { service.onData(replicatedEntityStreamIn); } else { console.error('Unknown message received before init %o', replicatedEntityStreamIn); call.write({ failure: { description: 'Unknown message received before init', }, }); call.end(); } }); call.on('end', () => { if (service != null) { service.onEnd(); } else { call.end(); } }); } } class ReplicatedEntitySupport { constructor(root, service, handlers, allComponents) { this.root = root; this.service = service; this.anySupport = new AnySupport(this.root); this.commandHandlers = handlers.commandHandlers; this.onStateSet = handlers.onStateSet; this.defaultValue = handlers.defaultValue; this.allComponents = allComponents; } create(call, init) { const handler = new ReplicatedEntityHandler(this, call, init.entityId); if (init.delta) { handler.handleInitialDelta(init.delta); } return handler; } } /* * Handler for a single Replicated Entity. */ class ReplicatedEntityHandler { constructor(support, call, entityId) { this.entity = support; this.call = call; this.entityId = entityId; this.currentState = null; this.streamId = Math.random().toString(16).substr(2, 7); this.commandHelper = new CommandHelper(this.entityId, support.service, this.streamId, call, this.commandHandlerFactory.bind(this), support.allComponents, debug); this.streamDebug('Started new stream'); } commandHandlerFactory(commandName, grpcMethod) { if (this.entity.commandHandlers.hasOwnProperty(commandName)) { return (command, ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () { /** * Context for a Replicated Entity command handler. * * @interface module:akkaserverless.replicatedentity.ReplicatedEntityCommandContext * @extends module:akkaserverless.replicatedentity.StateManagementContext * @extends module:akkaserverless.CommandContext * @extends module:akkaserverless.EntityContext */ this.addStateManagementToContext(ctx); const userReply = yield this.entity.commandHandlers[commandName](command, ctx.context); this.setStateActionOnReply(ctx); return userReply; }); } else { return null; } } setStateActionOnReply(ctx) { if (ctx.deleted) { ctx.commandDebug('Deleting entity'); ctx.reply.stateAction = { delete: {}, }; this.currentState = null; } else if (this.currentState !== null) { const delta = this.currentState.getAndResetDelta(); if (delta != null) { ctx.commandDebug('Updating entity'); ctx.reply.stateAction = { update: delta, }; } } } addStateManagementToContext(ctx) { ctx.deleted = false; ctx.noState = this.currentState === null; ctx.defaultValue = false; if (ctx.noState) { this.currentState = this.entity.defaultValue(); if (this.currentState !== null) { this.entity.onStateSet(this.currentState, this.entityId); ctx.defaultValue = true; } } /** * Context that allows managing a Replicated Entity's state. * * @interface module:akkaserverless.replicatedentity.StateManagementContext */ /** * Delete this Replicated Entity. * * @function module:akkaserverless.replicatedentity.StateManagementContext#delete */ ctx.context.delete = () => { ctx.ensureActive(); if (this.currentState === null) { throw new Error("Can't delete entity that hasn't been created."); } else if (ctx.noState) { this.currentState = null; } else { ctx.deleted = true; } }; /** * The Replicated Data state for a Replicated Entity. * It may only be set once, if it's already set, an error will be thrown. * * @name module:akkaserverless.replicatedentity.StateManagementContext#state * @type {module:akkaserverless.replicatedentity.ReplicatedData} */ Object.defineProperty(ctx.context, 'state', { get: () => { ctx.ensureActive(); return this.currentState; }, set: (state) => { ctx.ensureActive(); if (this.currentState !== null) { throw new Error("Cannot create a new Replicated Entity state after it's been created."); } else if (typeof state.getAndResetDelta !== 'function') { throw new Error(util.format('%o is not a Replicated Data type', state)); } else { this.currentState = state; this.entity.onStateSet(this.currentState, this.entityId); } }, }); } streamDebug(msg, ...args) { debug('%s [%s] - ' + msg, ...[this.streamId, this.entityId].concat(args)); } handleInitialDelta(delta) { this.streamDebug('Handling initial delta for Replicated Data type %s', delta.delta); if (this.currentState === null) { this.currentState = replicatedData.createForDelta(delta); } this.currentState.applyDelta(delta, this.entity.anySupport, replicatedData.createForDelta); this.entity.onStateSet(this.currentState, this.entityId); } onData(replicatedEntityStreamIn) { return tslib_1.__awaiter(this, void 0, void 0, function* () { try { yield this.handleReplicatedEntityStreamIn(replicatedEntityStreamIn); } catch (err) { this.streamDebug('Error handling message, terminating stream: %o', replicatedEntityStreamIn); console.error(err); this.call.write({ failure: { commandId: 0, description: 'Fatal error handling message, check user container logs.', }, }); this.call.end(); } }); } handleReplicatedEntityStreamIn(replicatedEntityStreamIn) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (replicatedEntityStreamIn.delta && this.currentState === null) { yield this.handleInitialDelta(replicatedEntityStreamIn.delta); } else if (replicatedEntityStreamIn.delta) { this.streamDebug('Received delta for Replicated Data type %s', replicatedEntityStreamIn.delta.delta); this.currentState.applyDelta(replicatedEntityStreamIn.delta, this.entity.anySupport, replicatedData.createForDelta); } else if (replicatedEntityStreamIn.delete) { this.streamDebug('Received Replicated Entity delete'); this.currentState = null; } else if (replicatedEntityStreamIn.command) { yield this.commandHelper.handleCommand(replicatedEntityStreamIn.command); } else { this.call.write({ failure: { commandId: 0, description: util.format('Unknown message: %o', replicatedEntityStreamIn), }, }); this.call.end(); } }); } onEnd() { this.streamDebug('Stream terminating'); this.call.end(); } } module.exports = { ReplicatedEntityServices: ReplicatedEntityServices, ReplicatedEntitySupport: ReplicatedEntitySupport, ReplicatedEntityHandler: ReplicatedEntityHandler, }; //# sourceMappingURL=replicated-entity-support.js.map