UNPKG

@lightbend/akkaserverless-javascript-sdk

Version:
426 lines 19.2 kB
"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