@lightbend/akkaserverless-javascript-sdk
Version:
Akka Serverless JavaScript SDK
426 lines • 19.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GrpcStatus = exports.AkkaServerless = exports.ReplicatedWriteConsistency = void 0;
const tslib_1 = require("tslib");
/*
* 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 = tslib_1.__importStar(require("fs"));
const path = tslib_1.__importStar(require("path"));
const grpc = tslib_1.__importStar(require("@grpc/grpc-js"));
const settings = tslib_1.__importStar(require("../settings"));
const discovery = tslib_1.__importStar(require("../proto/akkaserverless/protocol/discovery_pb"));
const discovery_grpc = tslib_1.__importStar(require("../proto/akkaserverless/protocol/discovery_grpc_pb"));
const google_protobuf_empty_pb = tslib_1.__importStar(require("google-protobuf/google/protobuf/empty_pb"));
const package_info_1 = require("./package-info");
function loadJson(filename) {
return JSON.parse(fs.readFileSync(filename).toString());
}
const userPkgJson = path.join(process.cwd(), 'package.json');
class ServiceInfo {
constructor(name, version, filename = userPkgJson) {
this.pkgName = 'unknown';
this.pkgVersion = '0.0.0';
if (!name || !version) {
this.loadFromPkg(filename);
}
this.name = name || this.pkgName;
this.version = version || this.pkgVersion;
}
loadFromPkg(filename = userPkgJson) {
const json = loadJson(filename);
this.pkgName = json.name;
this.pkgVersion = json.version;
}
}
var ReplicatedWriteConsistency;
(function (ReplicatedWriteConsistency) {
/**
* Updates will only be written to the local replica immediately, and then asynchronously
* distributed to other replicas in the background.
*/
ReplicatedWriteConsistency[ReplicatedWriteConsistency["LOCAL"] = 0] = "LOCAL";
/**
* Updates will be written immediately to a majority of replicas, and then asynchronously
* distributed to remaining replicas in the background.
*/
ReplicatedWriteConsistency[ReplicatedWriteConsistency["MAJORITY"] = 1] = "MAJORITY";
/**
* Updates will be written immediately to all replicas.
*/
ReplicatedWriteConsistency[ReplicatedWriteConsistency["ALL"] = 2] = "ALL";
})(ReplicatedWriteConsistency = exports.ReplicatedWriteConsistency || (exports.ReplicatedWriteConsistency = {}));
class DocLink {
constructor(baseUrl = 'https://developer.lightbend.com/docs/akka-serverless/') {
this.baseUrl = baseUrl;
this.specificCodes = new Map([
['AS-00112', 'javascript/views.html#changing'],
['AS-00402', 'javascript/topic-eventing.html'],
['AS-00406', 'javascript/topic-eventing.html'],
['AS-00414', 'javascript/entity-eventing.html'],
// TODO: docs for value entity eventing (https://github.com/lightbend/akkaserverless-javascript-sdk/issues/103)
// ['AS-00415', 'javascript/entity-eventing.html'],
]);
this.codeCategories = new Map([
['AS-001', 'javascript/views.html'],
['AS-002', 'javascript/value-entity.html'],
['AS-003', 'javascript/eventsourced.html'],
['AS-004', 'javascript/'],
['AS-005', 'javascript/'],
['AS-006', 'javascript/proto.html#_transcoding_http'], // all HTTP API errors
]);
this.specificCodes.forEach((value, key) => key.length >= 6);
}
getLink(code) {
const shortCode = code.substr(0, 6);
if (this.specificCodes.has(code)) {
return `${this.baseUrl}${this.specificCodes.get(code)}`;
}
else if (this.codeCategories.has(shortCode)) {
return `${this.baseUrl}${this.codeCategories.get(shortCode)}`;
}
else {
return '';
}
}
}
class SourceFormatter {
constructor(location) {
this.location = location;
}
getLocationString(components) {
var _a, _b;
if (this.location.getEndLine() === 0 && this.location.getEndCol() === 0) {
// It's been sent without line/col data
return `At ${this.location.getFileName}`;
}
// First, we need to location the protobuf file that it's from. To do that, we need to look in the include dirs
// of each entity.
for (const component of components) {
for (const includeDir of (_b = (_a = component.options) === null || _a === void 0 ? void 0 : _a.includeDirs) !== null && _b !== void 0 ? _b : []) {
const file = path.resolve(includeDir, this.location.getFileName());
if (fs.existsSync(file)) {
const lines = fs
.readFileSync(file)
.toString('utf-8')
.split(/\r?\n/)
.slice(this.location.getStartLine(), this.location.getEndLine() + 1);
let content = '';
if (lines.length > 1) {
content = lines.join('\n');
}
else if (lines.length === 1) {
const line = lines[0];
content = line + '\n';
for (let i = 0; i < Math.min(line.length, this.location.getStartCol()); i++) {
if (line.charAt(i) === '\t') {
content += '\t';
}
else {
content += ' ';
}
}
content += '^';
}
return `At ${this.location.getFileName()}:${this.location.getStartLine() + 1}:${this.location.getStartCol() + 1}:\n${content}`;
}
}
}
return `At ${this.location.getFileName()}:${this.location.getStartLine() + 1}:${this.location.getStartCol() + 1}`;
}
}
/**
* Akka Serverless service.
*
* @param options - the options for starting the service
*/
class AkkaServerless {
constructor(options) {
this.address = process.env.HOST || '127.0.0.1';
this.port = (process.env.PORT ? parseInt(process.env.PORT) : undefined) || 8080;
this.descriptorSetPath = 'user-function.desc';
this.packageInfo = new package_info_1.PackageInfo();
this.components = [];
this.runtime = `${process.title} ${process.version}`;
this.protocolMajorVersion = parseInt(settings.protocolVersion().major);
this.protocolMinorVersion = parseInt(settings.protocolVersion().minor);
this.docLink = new DocLink();
this.proxySeen = false;
this.proxyHasTerminated = false;
this.waitingForProxyTermination = false;
this.devMode = false;
if (options === null || options === void 0 ? void 0 : options.descriptorSetPath) {
this.descriptorSetPath = options.descriptorSetPath;
}
this.service = new ServiceInfo(options === null || options === void 0 ? void 0 : options.serviceName, options === null || options === void 0 ? void 0 : options.serviceVersion);
try {
this.proto = fs.readFileSync(this.descriptorSetPath);
}
catch (e) {
throw new Error(`Unable to read protobuf descriptor from: ${this.descriptorSetPath}`);
}
this.server = new grpc.Server();
}
/**
* Add one or more components to this AkkaServerless service.
*
* @param components - the components to add
* @returns this AkkaServerless service
*/
addComponent(...components) {
this.components = this.components.concat(components);
return this;
}
getComponents() {
return this.components;
}
afterStart(port) {
console.log('Akka Serverless service started on ' + this.address + ':' + port);
process.on('SIGTERM', () => {
if (!this.proxySeen || this.proxyHasTerminated || this.devMode) {
console.debug('Got SIGTERM. Shutting down');
this.terminate();
}
else {
console.debug('Got SIGTERM. But did not yet see proxy terminating, deferring shutdown until proxy stops');
// no timeout because process will be SIGKILLed anyway if it does not get the proxy termination in time
this.waitingForProxyTermination = true;
}
});
}
/**
* Start the Akka Serverless service.
* @param binding - optional address/port binding to start the service on
* @returns a Promise of the bound port for this service
*/
start(binding) {
if (binding) {
if (binding.address) {
this.address = binding.address;
}
if (binding.port) {
this.port = binding.port;
}
}
const allComponentsMap = {};
this.components.forEach((component) => {
var _a;
allComponentsMap[(_a = component.serviceName) !== null && _a !== void 0 ? _a : 'undefined'] =
component.service;
});
const componentTypes = {};
this.components.forEach((component) => {
if (component.register) {
const componentServices = component.register(allComponentsMap);
componentTypes[componentServices.componentType()] = componentServices;
}
});
Object.values(componentTypes).forEach((services) => {
services.register(this.server);
});
const discoveryServer = this.getDiscoveryServer();
this.server.addService(discovery_grpc.DiscoveryService, discoveryServer);
return new Promise((resolve, reject) => {
this.server.bindAsync(`${this.address}:${this.port}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
if (err) {
console.error(`Server error: ${err.message}`);
reject(err);
}
else {
console.log(`Server bound on port: ${port}`);
this.server.start();
this.afterStart(port);
resolve(port);
}
});
});
}
docLinkFor(code) {
return this.docLink.getLink(code);
}
formatSource(location) {
return new SourceFormatter(location).getLocationString(this.components);
}
getDiscoveryServer() {
const that = this;
const discoveryServer = {
discover(call, callback) {
const result = that.discoveryLogic(call.request);
callback(null, result);
},
reportError(call, callback) {
const msg = that.reportErrorLogic(call.request.getCode(), call.request.getMessage(), call.request.getDetail(), call.request.getSourceLocationsList());
console.error(msg);
callback(null, new google_protobuf_empty_pb.Empty());
},
proxyTerminated(call, callback) {
that.proxyTerminatedLogic();
callback(null, new google_protobuf_empty_pb.Empty());
},
healthCheck(call, callback) {
callback(null, new google_protobuf_empty_pb.Empty());
},
};
return discoveryServer;
}
/**
* Shut down the Akka Serverless service.
*/
shutdown() {
this.tryShutdown(() => {
console.log('Akka Serverless service has shutdown.');
});
}
/**
* Shut down the Akka Serverless service.
*
* @param callback - shutdown callback, accepting possible error
*/
tryShutdown(callback) {
this.server.tryShutdown(callback);
}
terminate() {
this.server.forceShutdown();
process.exit(0);
}
reportErrorLogic(code, message, detail, locations) {
let msg = `Error reported from Akka Serverless system: ${code} ${message}`;
if (detail) {
msg += `\n\n${detail}`;
}
if (code) {
const docLink = this.docLink.getLink(code);
if (docLink.length > 0)
msg += `\nSee documentation: ${this.docLink.getLink(code)}`;
for (const location of locations || []) {
msg += `\n\n${this.formatSource(location)}`;
}
}
return msg;
}
// detect hybrid proxy version probes when protocol version 0.0 (or undefined)
isVersionProbe(proxyInfo) {
return (!proxyInfo.getProtocolMajorVersion() &&
!proxyInfo.getProtocolMinorVersion());
}
discoveryLogic(proxyInfo) {
const serviceInfo = new discovery.ServiceInfo()
.setServiceName(this.service.name)
.setServiceVersion(this.service.version)
.setServiceRuntime(this.runtime)
.setSupportLibraryName(this.packageInfo.name)
.setSupportLibraryVersion(this.packageInfo.version)
.setProtocolMajorVersion(this.protocolMajorVersion)
.setProtocolMinorVersion(this.protocolMinorVersion);
const spec = new discovery.Spec().setServiceInfo(serviceInfo);
if (this.isVersionProbe(proxyInfo)) {
// only (silently) send service info for hybrid proxy version probe
}
else {
this.proxySeen = true;
this.devMode = proxyInfo.getDevMode();
this.proxyHasTerminated = false;
console.debug(`Discover call with info ${proxyInfo}, sending ${this.components.length} components`);
const components = this.components.map((component) => {
var _a;
const res = new discovery.Component();
res.setServiceName(component.serviceName);
res.setComponentType(component.componentType());
if (res.getComponentType().indexOf('Entities') > -1) {
// entities has EntityOptions / EntitySettings
const entityOptions = component.options;
const entitySettings = new discovery.EntitySettings();
if (entityOptions.entityType) {
entitySettings.setEntityType(entityOptions.entityType);
}
if ((_a = entityOptions.entityPassivationStrategy) === null || _a === void 0 ? void 0 : _a.timeout) {
const ps = new discovery.PassivationStrategy().setTimeout(new discovery.TimeoutPassivationStrategy().setTimeout(entityOptions.entityPassivationStrategy.timeout));
entitySettings.setPassivationStrategy(ps);
}
if (entityOptions.forwardHeaders) {
entitySettings.setForwardHeadersList(entityOptions.forwardHeaders);
}
if (entityOptions.replicatedWriteConsistency) {
const replicatedEntitySettings = new discovery.ReplicatedEntitySettings();
let writeConsistency = discovery.ReplicatedWriteConsistency
.REPLICATED_WRITE_CONSISTENCY_LOCAL_UNSPECIFIED;
switch (entityOptions.replicatedWriteConsistency) {
case ReplicatedWriteConsistency.ALL:
writeConsistency =
discovery.ReplicatedWriteConsistency
.REPLICATED_WRITE_CONSISTENCY_ALL;
break;
case ReplicatedWriteConsistency.MAJORITY:
writeConsistency =
discovery.ReplicatedWriteConsistency
.REPLICATED_WRITE_CONSISTENCY_MAJORITY;
break;
default:
writeConsistency =
discovery.ReplicatedWriteConsistency
.REPLICATED_WRITE_CONSISTENCY_LOCAL_UNSPECIFIED;
}
replicatedEntitySettings.setWriteConsistency(writeConsistency);
entitySettings.setReplicatedEntity(replicatedEntitySettings);
}
res.setEntity(entitySettings);
}
else {
// other components has ComponentOptions / GenericComponentSettings
const componentOptions = component.options;
const componentSettings = new discovery.GenericComponentSettings();
if (componentOptions.forwardHeaders) {
componentSettings.setForwardHeadersList(componentOptions.forwardHeaders);
}
res.setComponent(componentSettings);
}
return res;
});
spec.setProto(this.proto).setComponentsList(components);
}
return spec;
}
proxyTerminatedLogic() {
this.proxyHasTerminated = true;
if (this.waitingForProxyTermination) {
this.terminate();
}
}
}
exports.AkkaServerless = AkkaServerless;
/**
* The GRPC status codes.
*/
var GrpcStatus;
(function (GrpcStatus) {
GrpcStatus[GrpcStatus["Ok"] = 0] = "Ok";
GrpcStatus[GrpcStatus["Cancelled"] = 1] = "Cancelled";
GrpcStatus[GrpcStatus["Unknown"] = 2] = "Unknown";
GrpcStatus[GrpcStatus["InvalidArgument"] = 3] = "InvalidArgument";
GrpcStatus[GrpcStatus["DeadlineExceeded"] = 4] = "DeadlineExceeded";
GrpcStatus[GrpcStatus["NotFound"] = 5] = "NotFound";
GrpcStatus[GrpcStatus["AlreadyExists"] = 6] = "AlreadyExists";
GrpcStatus[GrpcStatus["PermissionDenied"] = 7] = "PermissionDenied";
GrpcStatus[GrpcStatus["ResourceExhausted"] = 8] = "ResourceExhausted";
GrpcStatus[GrpcStatus["FailedPrecondition"] = 9] = "FailedPrecondition";
GrpcStatus[GrpcStatus["Aborted"] = 10] = "Aborted";
GrpcStatus[GrpcStatus["OutOfRange"] = 11] = "OutOfRange";
GrpcStatus[GrpcStatus["Unimplemented"] = 12] = "Unimplemented";
GrpcStatus[GrpcStatus["Internal"] = 13] = "Internal";
GrpcStatus[GrpcStatus["Unavailable"] = 14] = "Unavailable";
GrpcStatus[GrpcStatus["DataLoss"] = 15] = "DataLoss";
GrpcStatus[GrpcStatus["Unauthenticated"] = 16] = "Unauthenticated";
})(GrpcStatus = exports.GrpcStatus || (exports.GrpcStatus = {}));
//# sourceMappingURL=akkaserverless.js.map