@grpc/grpc-js
Advanced tools
Comparing version 0.5.2 to 0.5.3
@@ -270,2 +270,10 @@ "use strict"; | ||
}); | ||
/* If the underlying TLS or TCP connection closes, we want to end the | ||
* call with an UNAVAILABLE status to match the behavior of the other | ||
* library. In this handler we don't wait for trailers before ending the | ||
* call. This should ensure that this endCall happens sooner than the one | ||
* in the stream.on('close', ...) handler. */ | ||
stream.session.socket.on('close', () => { | ||
this.endCall({ code: constants_1.Status.UNAVAILABLE, details: 'Connection dropped', metadata: new metadata_1.Metadata() }); | ||
}); | ||
if (!this.pendingRead) { | ||
@@ -272,0 +280,0 @@ stream.pause(); |
@@ -11,5 +11,6 @@ /// <reference types="node" /> | ||
import { Metadata } from './metadata'; | ||
import { Server } from './server'; | ||
import { Server, UntypedHandleCall, UntypedServiceImplementation } from './server'; | ||
import { KeyCertPair, ServerCredentials } from './server-credentials'; | ||
import { StatusBuilder } from './status-builder'; | ||
import { ServerUnaryCall, ServerReadableStream, ServerWritableStream, ServerDuplexStream } from './server-call'; | ||
export interface OAuth2Client { | ||
@@ -39,3 +40,3 @@ getRequestMetadata: (url: string, callback: (err: Error | null, headers?: { | ||
export declare const waitForClientReady: (client: Client, deadline: Deadline, callback: (error?: Error | undefined) => void) => void; | ||
export { ChannelCredentials, CallCredentials, Deadline, Serialize as serialize, Deserialize as deserialize, ClientUnaryCall, ClientReadableStream, ClientWritableStream, ClientDuplexStream, CallOptions, StatusObject, ServiceError, }; | ||
export { ChannelCredentials, CallCredentials, Deadline, Serialize as serialize, Deserialize as deserialize, ClientUnaryCall, ClientReadableStream, ClientWritableStream, ClientDuplexStream, CallOptions, StatusObject, ServiceError, ServerUnaryCall, ServerReadableStream, ServerWritableStream, ServerDuplexStream, UntypedHandleCall, UntypedServiceImplementation }; | ||
export declare type Call = ClientUnaryCall | ClientReadableStream<any> | ClientWritableStream<any> | ClientDuplexStream<any, any>; | ||
@@ -42,0 +43,0 @@ export declare type MetadataListener = (metadata: Metadata, next: Function) => void; |
@@ -19,5 +19,2 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
/* This file is an implementation of gRFC A24: | ||
* https://github.com/grpc/proposal/blob/master/A24-lb-policy-config.md */ | ||
const util_1 = require("util"); | ||
/* In these functions we assume the input came from a JSON object. Therefore we | ||
@@ -27,3 +24,3 @@ * expect that the prototype is uninteresting and that `in` can be used | ||
function validateXdsConfig(xds) { | ||
if (!('balancerName' in xds) || !util_1.isString(xds.balancerName)) { | ||
if (!('balancerName' in xds) || typeof xds.balancerName !== 'string') { | ||
throw new Error('Invalid xds config: invalid balancerName'); | ||
@@ -34,6 +31,6 @@ } | ||
childPolicy: [], | ||
fallbackPolicy: [] | ||
fallbackPolicy: [], | ||
}; | ||
if ('childPolicy' in xds) { | ||
if (!util_1.isArray(xds.childPolicy)) { | ||
if (!Array.isArray(xds.childPolicy)) { | ||
throw new Error('Invalid xds config: invalid childPolicy'); | ||
@@ -46,3 +43,3 @@ } | ||
if ('fallbackPolicy' in xds) { | ||
if (!util_1.isArray(xds.fallbackPolicy)) { | ||
if (!Array.isArray(xds.fallbackPolicy)) { | ||
throw new Error('Invalid xds config: invalid fallbackPolicy'); | ||
@@ -58,6 +55,6 @@ } | ||
const grpcLbConfig = { | ||
childPolicy: [] | ||
childPolicy: [], | ||
}; | ||
if ('childPolicy' in grpclb) { | ||
if (!util_1.isArray(grpclb.childPolicy)) { | ||
if (!Array.isArray(grpclb.childPolicy)) { | ||
throw new Error('Invalid xds config: invalid childPolicy'); | ||
@@ -64,0 +61,0 @@ } |
@@ -0,1 +1,5 @@ | ||
/** | ||
* Set up the DNS resolver class by registering it as the handler for the | ||
* "dns:" prefix and as the default resolver. | ||
*/ | ||
export declare function setup(): void; |
@@ -20,18 +20,56 @@ "use strict"; | ||
const dns = require("dns"); | ||
const semver = require("semver"); | ||
const util = require("util"); | ||
const service_config_1 = require("./service-config"); | ||
const constants_1 = require("./constants"); | ||
const metadata_1 = require("./metadata"); | ||
/* These regular expressions match IP addresses with optional ports in different | ||
* formats. In each case, capture group 1 contains the address, and capture | ||
* group 2 contains the port number, if present */ | ||
const IPv4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/; | ||
const IPv6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i; | ||
const IPv6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i; | ||
/** | ||
* Matches 4 groups of up to 3 digits each, separated by periods, optionally | ||
* followed by a colon and a number. | ||
*/ | ||
const IPV4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/; | ||
/** | ||
* Matches any number of groups of up to 4 hex digits (case insensitive) | ||
* separated by 1 or more colons. This variant does not match a port number. | ||
*/ | ||
const IPV6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i; | ||
/** | ||
* Matches the same as the IPv6_REGEX, surrounded by square brackets, and | ||
* optionally followed by a colon and a number. | ||
*/ | ||
const IPV6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i; | ||
/** | ||
* Matches `[dns:][//authority/]host[:port]`, where `authority` and `host` are | ||
* both arbitrary sequences of alphanumeric characters and `port` is a sequence | ||
* of digits. Group 1 contains the hostname and group 2 contains the port | ||
* number if provided. | ||
*/ | ||
const DNS_REGEX = /^(?:dns:)?(?:\/\/\w+\/)?(\w+)(?::(\d+))?$/; | ||
/** | ||
* The default TCP port to connect to if not explicitly specified in the target. | ||
*/ | ||
const DEFAULT_PORT = '443'; | ||
/** | ||
* The range of Node versions in which the Node issue | ||
* https://github.com/nodejs/node/issues/28216 has been fixed. In other | ||
* versions, IPv6 literal addresses cannot be used to establish HTTP/2 | ||
* connections. | ||
*/ | ||
const IPV6_SUPPORT_RANGE = '>= 12.6'; | ||
const resolve4Promise = util.promisify(dns.resolve4); | ||
const resolve6Promise = util.promisify(dns.resolve6); | ||
/** | ||
* Attempt to parse a target string as an IP address | ||
* @param target | ||
* @return An "IP:port" string if parsing was successful, `null` otherwise | ||
*/ | ||
function parseIP(target) { | ||
/* These three regular expressions are all mutually exclusive, so we just | ||
* want the first one that matches the target string, if any do. */ | ||
const match = IPv4_REGEX.exec(target) || IPv6_REGEX.exec(target) || IPv6_BRACKET_REGEX.exec(target); | ||
const match = IPV4_REGEX.exec(target) || | ||
IPV6_REGEX.exec(target) || | ||
IPV6_BRACKET_REGEX.exec(target); | ||
if (match === null) { | ||
@@ -50,6 +88,10 @@ return null; | ||
} | ||
/** | ||
* Merge any number of arrays into a single alternating array | ||
* @param arrays | ||
*/ | ||
function mergeArrays(...arrays) { | ||
const result = []; | ||
for (let i = 0; i < Math.max.apply(null, arrays.map((array) => array.length)); i++) { | ||
for (let array of arrays) { | ||
for (let i = 0; i < Math.max.apply(null, arrays.map(array => array.length)); i++) { | ||
for (const array of arrays) { | ||
if (i < array.length) { | ||
@@ -62,2 +104,5 @@ result.push(array[i]); | ||
} | ||
/** | ||
* Resolver implementation that handles DNS names and IP addresses. | ||
*/ | ||
class DnsResolver { | ||
@@ -86,4 +131,7 @@ constructor(target, listener) { | ||
this.percentage = Math.random() * 100; | ||
this.startResolution(); | ||
} | ||
/** | ||
* If the target is an IP address, just provide that address as a result. | ||
* Otherwise, initiate A, AAAA, and TXT | ||
*/ | ||
startResolution() { | ||
@@ -98,5 +146,14 @@ if (this.ipResult !== null) { | ||
const hostname = this.dnsHostname; | ||
const Aresult = resolve4Promise(hostname); | ||
const AAAAresult = resolve6Promise(hostname); | ||
const TXTresult = new Promise((resolve, reject) => { | ||
const aResult = resolve4Promise(hostname); | ||
let aaaaResult; | ||
if (semver.satisfies(process.version, IPV6_SUPPORT_RANGE)) { | ||
aaaaResult = resolve6Promise(hostname); | ||
} | ||
else { | ||
aaaaResult = Promise.resolve([]); | ||
} | ||
/* We handle the TXT query promise differently than the others because | ||
* the name resolution attempt as a whole is a success even if the TXT | ||
* lookup fails */ | ||
const txtResult = new Promise((resolve, reject) => { | ||
dns.resolveTxt(hostname, (err, records) => { | ||
@@ -111,23 +168,37 @@ if (err) { | ||
}); | ||
this.pendingResultPromise = Promise.all([Aresult, AAAAresult, TXTresult]); | ||
this.pendingResultPromise.then(([Arecord, AAAArecord, TXTrecord]) => { | ||
this.pendingResultPromise = Promise.all([aResult, aaaaResult, txtResult]); | ||
this.pendingResultPromise.then(([aRecord, aaaaRecord, txtRecord]) => { | ||
this.pendingResultPromise = null; | ||
const allAddresses = mergeArrays(AAAArecord, Arecord); | ||
aRecord = aRecord.map(value => `${value}:${this.port}`); | ||
aaaaRecord = aaaaRecord.map(value => `[${value}]:${this.port}`); | ||
const allAddresses = mergeArrays(aaaaRecord, aRecord); | ||
let serviceConfig = null; | ||
let serviceConfigError = null; | ||
if (TXTrecord instanceof Error) { | ||
serviceConfigError = TXTrecord; | ||
if (txtRecord instanceof Error) { | ||
serviceConfigError = { | ||
code: constants_1.Status.UNAVAILABLE, | ||
details: 'TXT query failed', | ||
metadata: new metadata_1.Metadata(), | ||
}; | ||
} | ||
else { | ||
try { | ||
serviceConfig = service_config_1.extractAndSelectServiceConfig(TXTrecord, this.percentage); | ||
serviceConfig = service_config_1.extractAndSelectServiceConfig(txtRecord, this.percentage); | ||
} | ||
catch (err) { | ||
serviceConfigError = err; | ||
serviceConfigError = { | ||
code: constants_1.Status.UNAVAILABLE, | ||
details: 'Parsing service config failed', | ||
metadata: new metadata_1.Metadata(), | ||
}; | ||
} | ||
} | ||
this.listener.onSuccessfulResolution(allAddresses, serviceConfig, serviceConfigError); | ||
}, (err) => { | ||
}, err => { | ||
this.pendingResultPromise = null; | ||
this.listener.onError(err); | ||
this.listener.onError({ | ||
code: constants_1.Status.UNAVAILABLE, | ||
details: 'Name resolution failed', | ||
metadata: new metadata_1.Metadata(), | ||
}); | ||
}); | ||
@@ -141,3 +212,25 @@ } | ||
} | ||
/** | ||
* Get the default authority for the given target. For IP targets, that is | ||
* the IP address. For DNS targets, it is the hostname. | ||
* @param target | ||
*/ | ||
static getDefaultAuthority(target) { | ||
const ipMatch = IPV4_REGEX.exec(target) || | ||
IPV6_REGEX.exec(target) || | ||
IPV6_BRACKET_REGEX.exec(target); | ||
if (ipMatch) { | ||
return ipMatch[1]; | ||
} | ||
const dnsMatch = DNS_REGEX.exec(target); | ||
if (dnsMatch) { | ||
return dnsMatch[1]; | ||
} | ||
throw new Error(`Failed to parse target ${target}`); | ||
} | ||
} | ||
/** | ||
* Set up the DNS resolver class by registering it as the handler for the | ||
* "dns:" prefix and as the default resolver. | ||
*/ | ||
function setup() { | ||
@@ -144,0 +237,0 @@ resolver_1.registerResolver('dns:', DnsResolver); |
@@ -1,8 +0,35 @@ | ||
import { ServiceError } from "./call"; | ||
import { ServiceConfig } from "./service-config"; | ||
import { ServiceConfig } from './service-config'; | ||
import { StatusObject } from './call-stream'; | ||
/** | ||
* A listener object passed to the resolver's constructor that provides name | ||
* resolution updates back to the resolver's owner. | ||
*/ | ||
export interface ResolverListener { | ||
onSuccessfulResolution(addressList: string[], serviceConfig: ServiceConfig | null, serviceConfigError: Error | null): void; | ||
onError(error: ServiceError): void; | ||
/** | ||
* Called whenever the resolver has new name resolution results to report | ||
* @param addressList The new list of backend addresses | ||
* @param serviceConfig The new service configuration corresponding to the | ||
* `addressList`. Will be `null` if no service configuration was | ||
* retrieved or if the service configuration was invalid | ||
* @param serviceConfigError If non-`null`, indicates that the retrieved | ||
* service configuration was invalid | ||
*/ | ||
onSuccessfulResolution(addressList: string[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null): void; | ||
/** | ||
* Called whenever a name resolution attempt fails. | ||
* @param error Describes how resolution failed | ||
*/ | ||
onError(error: StatusObject): void; | ||
} | ||
/** | ||
* A resolver class that handles one or more of the name syntax schemes defined | ||
* in the [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md) | ||
*/ | ||
export interface Resolver { | ||
/** | ||
* Indicates that the caller wants new name resolution data. Calling this | ||
* function may eventually result in calling one of the `ResolverListener` | ||
* functions, but that is not guaranteed. Those functions will never be | ||
* called synchronously with the constructor or updateResolution. | ||
*/ | ||
updateResolution(): void; | ||
@@ -12,5 +39,37 @@ } | ||
new (target: string, listener: ResolverListener): Resolver; | ||
/** | ||
* Get the default authority for a target. This loosely corresponds to that | ||
* target's hostname. Throws an error if this resolver class cannot parse the | ||
* `target`. | ||
* @param target | ||
*/ | ||
getDefaultAuthority(target: string): string; | ||
} | ||
/** | ||
* Register a resolver class to handle target names prefixed with the `prefix` | ||
* string. This prefix should correspond to a URI scheme name listed in the | ||
* [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md) | ||
* @param prefix | ||
* @param resolverClass | ||
*/ | ||
export declare function registerResolver(prefix: string, resolverClass: ResolverConstructor): void; | ||
/** | ||
* Register a default resolver to handle target names that do not start with | ||
* any registered prefix. | ||
* @param resolverClass | ||
*/ | ||
export declare function registerDefaultResolver(resolverClass: ResolverConstructor): void; | ||
/** | ||
* Create a name resolver for the specified target, if possible. Throws an | ||
* error if no such name resolver can be created. | ||
* @param target | ||
* @param listener | ||
*/ | ||
export declare function createResolver(target: string, listener: ResolverListener): Resolver; | ||
/** | ||
* Get the default authority for the specified target, if possible. Throws an | ||
* error if no registered name resolver can parse that target string. | ||
* @param target | ||
*/ | ||
export declare function getDefaultAuthority(target: string): string; | ||
export declare function registerAll(): void; |
@@ -19,4 +19,12 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const resolver_dns = require("./resolver-dns"); | ||
const registeredResolvers = {}; | ||
let defaultResolver = null; | ||
/** | ||
* Register a resolver class to handle target names prefixed with the `prefix` | ||
* string. This prefix should correspond to a URI scheme name listed in the | ||
* [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md) | ||
* @param prefix | ||
* @param resolverClass | ||
*/ | ||
function registerResolver(prefix, resolverClass) { | ||
@@ -26,2 +34,7 @@ registeredResolvers[prefix] = resolverClass; | ||
exports.registerResolver = registerResolver; | ||
/** | ||
* Register a default resolver to handle target names that do not start with | ||
* any registered prefix. | ||
* @param resolverClass | ||
*/ | ||
function registerDefaultResolver(resolverClass) { | ||
@@ -31,2 +44,8 @@ defaultResolver = resolverClass; | ||
exports.registerDefaultResolver = registerDefaultResolver; | ||
/** | ||
* Create a name resolver for the specified target, if possible. Throws an | ||
* error if no such name resolver can be created. | ||
* @param target | ||
* @param listener | ||
*/ | ||
function createResolver(target, listener) { | ||
@@ -41,5 +60,26 @@ for (const prefix of Object.keys(registeredResolvers)) { | ||
} | ||
throw new Error('No resolver could be created for the provided target'); | ||
throw new Error(`No resolver could be created for target ${target}`); | ||
} | ||
exports.createResolver = createResolver; | ||
/** | ||
* Get the default authority for the specified target, if possible. Throws an | ||
* error if no registered name resolver can parse that target string. | ||
* @param target | ||
*/ | ||
function getDefaultAuthority(target) { | ||
for (const prefix of Object.keys(registerDefaultResolver)) { | ||
if (target.startsWith(prefix)) { | ||
return registeredResolvers[prefix].getDefaultAuthority(target); | ||
} | ||
} | ||
if (defaultResolver !== null) { | ||
return defaultResolver.getDefaultAuthority(target); | ||
} | ||
throw new Error(`Invalid target ${target}`); | ||
} | ||
exports.getDefaultAuthority = getDefaultAuthority; | ||
function registerAll() { | ||
resolver_dns.setup(); | ||
} | ||
exports.registerAll = registerAll; | ||
//# sourceMappingURL=resolver.js.map |
@@ -5,6 +5,8 @@ /// <reference types="node" /> | ||
import { Duplex, Readable, Writable } from 'stream'; | ||
import { ServiceError } from './call'; | ||
import { StatusObject } from './call-stream'; | ||
import { Deserialize, Serialize } from './make-client'; | ||
import { Metadata } from './metadata'; | ||
import { ObjectReadable, ObjectWritable } from './object-stream'; | ||
export declare type ServerStatusResponse = Partial<StatusObject>; | ||
export declare type ServerErrorResponse = ServerStatusResponse & Error; | ||
export declare type ServerSurfaceCall = { | ||
@@ -18,7 +20,7 @@ cancelled: boolean; | ||
}; | ||
export declare type ServerReadableStream<RequestType, ResponseType> = ServerSurfaceCall & Readable; | ||
export declare type ServerWritableStream<RequestType, ResponseType> = ServerSurfaceCall & Writable & { | ||
export declare type ServerReadableStream<RequestType, ResponseType> = ServerSurfaceCall & ObjectReadable<RequestType>; | ||
export declare type ServerWritableStream<RequestType, ResponseType> = ServerSurfaceCall & ObjectWritable<ResponseType> & { | ||
request: RequestType | null; | ||
}; | ||
export declare type ServerDuplexStream<RequestType, ResponseType> = ServerSurfaceCall & Duplex; | ||
export declare type ServerDuplexStream<RequestType, ResponseType> = ServerSurfaceCall & ObjectReadable<RequestType> & ObjectWritable<ResponseType>; | ||
export declare class ServerUnaryCallImpl<RequestType, ResponseType> extends EventEmitter implements ServerUnaryCall<RequestType, ResponseType> { | ||
@@ -68,3 +70,3 @@ private call; | ||
} | ||
export declare type sendUnaryData<ResponseType> = (error: ServiceError | null, value: ResponseType | null, trailer?: Metadata, flags?: number) => void; | ||
export declare type sendUnaryData<ResponseType> = (error: ServerErrorResponse | ServerStatusResponse | null, value: ResponseType | null, trailer?: Metadata, flags?: number) => void; | ||
export declare type handleUnaryCall<RequestType, ResponseType> = (call: ServerUnaryCall<RequestType, ResponseType>, callback: sendUnaryData<ResponseType>) => void; | ||
@@ -118,5 +120,5 @@ export declare type handleClientStreamingCall<RequestType, ResponseType> = (call: ServerReadableStream<RequestType, ResponseType>, callback: sendUnaryData<ResponseType>) => void; | ||
deserializeMessage(bytes: Buffer): Promise<RequestType>; | ||
sendUnaryMessage(err: ServiceError | null, value: ResponseType | null, metadata?: Metadata, flags?: number): Promise<void>; | ||
sendUnaryMessage(err: ServerErrorResponse | ServerStatusResponse | null, value: ResponseType | null, metadata?: Metadata, flags?: number): Promise<void>; | ||
sendStatus(statusObj: StatusObject): void; | ||
sendError(error: ServiceError): void; | ||
sendError(error: ServerErrorResponse | ServerStatusResponse): void; | ||
write(chunk: Buffer): boolean | undefined; | ||
@@ -123,0 +125,0 @@ resume(): void; |
@@ -310,12 +310,12 @@ "use strict"; | ||
code: constants_1.Status.UNKNOWN, | ||
details: error.hasOwnProperty('message') | ||
details: ('message' in error) | ||
? error.message | ||
: 'Unknown Error', | ||
metadata: error.hasOwnProperty('metadata') | ||
metadata: ('metadata' in error && error.metadata !== undefined) | ||
? error.metadata | ||
: new metadata_1.Metadata(), | ||
}; | ||
if (error.hasOwnProperty('code') && Number.isInteger(error.code)) { | ||
if ('code' in error && typeof error.code === 'number' && Number.isInteger(error.code)) { | ||
status.code = error.code; | ||
if (error.hasOwnProperty('details')) { | ||
if ('details' in error && typeof error.details === 'string') { | ||
status.details = error.details; | ||
@@ -322,0 +322,0 @@ } |
import { Deserialize, Serialize, ServiceDefinition } from './make-client'; | ||
import { HandleCall } from './server-call'; | ||
import { ServerCredentials } from './server-credentials'; | ||
export declare type UntypedHandleCall = HandleCall<any, any>; | ||
export interface UntypedServiceImplementation { | ||
[name: string]: UntypedHandleCall; | ||
} | ||
export declare class Server { | ||
@@ -11,3 +15,3 @@ private http2Server; | ||
addProtoService(): void; | ||
addService(service: ServiceDefinition, implementation: object): void; | ||
addService(service: ServiceDefinition, implementation: UntypedServiceImplementation): void; | ||
bind(port: string, creds: ServerCredentials): void; | ||
@@ -14,0 +18,0 @@ bindAsync(port: string, creds: ServerCredentials, callback: (error: Error | null, port: number) => void): void; |
@@ -69,3 +69,2 @@ "use strict"; | ||
} | ||
const implMap = implementation; | ||
serviceKeys.forEach(name => { | ||
@@ -90,6 +89,6 @@ const attrs = service[name]; | ||
} | ||
let implFn = implMap[name]; | ||
let implFn = implementation[name]; | ||
let impl; | ||
if (implFn === undefined && typeof attrs.originalName === 'string') { | ||
implFn = implMap[attrs.originalName]; | ||
implFn = implementation[attrs.originalName]; | ||
} | ||
@@ -96,0 +95,0 @@ if (implFn !== undefined) { |
@@ -30,3 +30,5 @@ import * as lbconfig from './load-balancing-config'; | ||
* @param percentage A number chosen from the range [0, 100) that is used to select which config to use | ||
* @return The service configuration to use, given the percentage value, or null if the service config | ||
* data has a valid format but none of the options match the current client. | ||
*/ | ||
export declare function extractAndSelectServiceConfig(txtRecord: string[][], percentage: number): ServiceConfig | null; |
@@ -21,17 +21,30 @@ "use strict"; | ||
* https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md | ||
* https://github.com/grpc/grpc/blob/master/doc/service_config.md */ | ||
* https://github.com/grpc/grpc/blob/master/doc/service_config.md. Each | ||
* function here takes an object with unknown structure and returns its | ||
* specific object type if the input has the right structure, and throws an | ||
* error otherwise. */ | ||
/* The any type is purposely used here. All functions validate their input at | ||
* runtime */ | ||
/* tslint:disable:no-any */ | ||
const lbconfig = require("./load-balancing-config"); | ||
const util_1 = require("util"); | ||
const os = require("os"); | ||
/** | ||
* Recognizes a number with up to 9 digits after the decimal point, followed by | ||
* an "s", representing a number of seconds. | ||
*/ | ||
const TIMEOUT_REGEX = /^\d+(\.\d{1,9})?s$/; | ||
/** | ||
* Client language name used for determining whether this client matches a | ||
* `ServiceConfigCanaryConfig`'s `clientLanguage` list. | ||
*/ | ||
const CLIENT_LANGUAGE_STRING = 'node'; | ||
function validateName(obj) { | ||
if (!('service' in obj) || !util_1.isString(obj.service)) { | ||
if (!('service' in obj) || typeof obj.service !== 'string') { | ||
throw new Error('Invalid method config name: invalid service'); | ||
} | ||
const result = { | ||
service: obj.service | ||
service: obj.service, | ||
}; | ||
if ('method' in obj) { | ||
if (util_1.isString(obj.method)) { | ||
if (typeof obj.method === 'string') { | ||
result.method = obj.method; | ||
@@ -47,5 +60,5 @@ } | ||
const result = { | ||
name: [] | ||
name: [], | ||
}; | ||
if (!('name' in obj) || !util_1.isArray(obj.name)) { | ||
if (!('name' in obj) || !Array.isArray(obj.name)) { | ||
throw new Error('Invalid method config: invalid name array'); | ||
@@ -57,3 +70,3 @@ } | ||
if ('waitForReady' in obj) { | ||
if (!util_1.isBoolean(obj.waitForReady)) { | ||
if (typeof obj.waitForReady !== 'boolean') { | ||
throw new Error('Invalid method config: invalid waitForReady'); | ||
@@ -64,3 +77,4 @@ } | ||
if ('timeout' in obj) { | ||
if (!util_1.isString(obj.timeout) || !TIMEOUT_REGEX.test(obj.timeout)) { | ||
if (!(typeof obj.timeout === 'string') || | ||
!TIMEOUT_REGEX.test(obj.timeout)) { | ||
throw new Error('Invalid method config: invalid timeout'); | ||
@@ -71,3 +85,3 @@ } | ||
if ('maxRequestBytes' in obj) { | ||
if (!util_1.isNumber(obj.maxRequestBytes)) { | ||
if (typeof obj.maxRequestBytes !== 'number') { | ||
throw new Error('Invalid method config: invalid maxRequestBytes'); | ||
@@ -78,3 +92,3 @@ } | ||
if ('maxResponseBytes' in obj) { | ||
if (!util_1.isNumber(obj.maxResponseBytes)) { | ||
if (typeof obj.maxResponseBytes !== 'number') { | ||
throw new Error('Invalid method config: invalid maxRequestBytes'); | ||
@@ -89,6 +103,6 @@ } | ||
loadBalancingConfig: [], | ||
methodConfig: [] | ||
methodConfig: [], | ||
}; | ||
if ('loadBalancingPolicy' in obj) { | ||
if (util_1.isString(obj.loadBalancingPolicy)) { | ||
if (typeof obj.loadBalancingPolicy === 'string') { | ||
result.loadBalancingPolicy = obj.loadBalancingPolicy; | ||
@@ -101,3 +115,3 @@ } | ||
if ('loadBalancingConfig' in obj) { | ||
if (util_1.isArray(obj.loadBalancingConfig)) { | ||
if (Array.isArray(obj.loadBalancingConfig)) { | ||
for (const config of obj.loadBalancingConfig) { | ||
@@ -112,3 +126,3 @@ result.loadBalancingConfig.push(lbconfig.validateConfig(config)); | ||
if ('methodConfig' in obj) { | ||
if (util_1.isArray(obj.methodConfig)) { | ||
if (Array.isArray(obj.methodConfig)) { | ||
for (const methodConfig of obj.methodConfig) { | ||
@@ -124,3 +138,4 @@ result.methodConfig.push(validateMethodConfig(methodConfig)); | ||
for (const seenName of seenMethodNames) { | ||
if (name.service === seenName.service && name.method === seenName.method) { | ||
if (name.service === seenName.service && | ||
name.method === seenName.method) { | ||
throw new Error(`Invalid service config: duplicate name ${name.service}/${name.method}`); | ||
@@ -139,9 +154,9 @@ } | ||
const result = { | ||
serviceConfig: validateServiceConfig(obj.serviceConfig) | ||
serviceConfig: validateServiceConfig(obj.serviceConfig), | ||
}; | ||
if ('clientLanguage' in obj) { | ||
if (util_1.isArray(obj.clientLanguage)) { | ||
if (Array.isArray(obj.clientLanguage)) { | ||
result.clientLanguage = []; | ||
for (const lang of obj.clientLanguage) { | ||
if (util_1.isString(lang)) { | ||
if (typeof lang === 'string') { | ||
result.clientLanguage.push(lang); | ||
@@ -159,6 +174,6 @@ } | ||
if ('clientHostname' in obj) { | ||
if (util_1.isArray(obj.clientHostname)) { | ||
if (Array.isArray(obj.clientHostname)) { | ||
result.clientHostname = []; | ||
for (const lang of obj.clientHostname) { | ||
if (util_1.isString(lang)) { | ||
if (typeof lang === 'string') { | ||
result.clientHostname.push(lang); | ||
@@ -176,3 +191,5 @@ } | ||
if ('percentage' in obj) { | ||
if (util_1.isNumber(obj.percentage) && 0 <= obj.percentage && obj.percentage <= 100) { | ||
if (typeof obj.percentage === 'number' && | ||
0 <= obj.percentage && | ||
obj.percentage <= 100) { | ||
result.percentage = obj.percentage; | ||
@@ -185,3 +202,8 @@ } | ||
// Validate that no unexpected fields are present | ||
const allowedFields = ['clientLanguage', 'percentage', 'clientHostname', 'serviceConfig']; | ||
const allowedFields = [ | ||
'clientLanguage', | ||
'percentage', | ||
'clientHostname', | ||
'serviceConfig', | ||
]; | ||
for (const field in obj) { | ||
@@ -195,3 +217,3 @@ if (!allowedFields.includes(field)) { | ||
function validateAndSelectCanaryConfig(obj, percentage) { | ||
if (!util_1.isArray(obj)) { | ||
if (!Array.isArray(obj)) { | ||
throw new Error('Invalid service config list'); | ||
@@ -203,6 +225,7 @@ } | ||
* config if the field value does not match the current client */ | ||
if (util_1.isNumber(validatedConfig.percentage) && percentage > validatedConfig.percentage) { | ||
if (typeof validatedConfig.percentage === 'number' && | ||
percentage > validatedConfig.percentage) { | ||
continue; | ||
} | ||
if (util_1.isArray(validatedConfig.clientHostname)) { | ||
if (Array.isArray(validatedConfig.clientHostname)) { | ||
let hostnameMatched = false; | ||
@@ -218,3 +241,3 @@ for (const hostname of validatedConfig.clientHostname) { | ||
} | ||
if (util_1.isArray(validatedConfig.clientLanguage)) { | ||
if (Array.isArray(validatedConfig.clientLanguage)) { | ||
let languageMatched = false; | ||
@@ -240,2 +263,4 @@ for (const language of validatedConfig.clientLanguage) { | ||
* @param percentage A number chosen from the range [0, 100) that is used to select which config to use | ||
* @return The service configuration to use, given the percentage value, or null if the service config | ||
* data has a valid format but none of the options match the current client. | ||
*/ | ||
@@ -245,3 +270,5 @@ function extractAndSelectServiceConfig(txtRecord, percentage) { | ||
if (record.length > 0 && record[0].startsWith('grpc_config=')) { | ||
const recordString = [record[0].substring('grpc_config='.length)].concat(record.slice(1)).join(''); | ||
/* Treat the list of strings in this record as a single string and remove | ||
* "grpc_config=" from the beginning. The rest should be a JSON string */ | ||
const recordString = record.join('').substring('grpc_config='.length); | ||
const recordJson = JSON.parse(recordString); | ||
@@ -248,0 +275,0 @@ return validateAndSelectCanaryConfig(recordJson, percentage); |
{ | ||
"name": "@grpc/grpc-js", | ||
"version": "0.5.2", | ||
"version": "0.5.3", | ||
"description": "gRPC Library for Node - pure JS implementation", | ||
@@ -19,9 +19,21 @@ "homepage": "https://grpc.io/", | ||
"@grpc/proto-loader": "^0.5.0", | ||
"@types/gulp": "^4.0.6", | ||
"@types/gulp-mocha": "0.0.32", | ||
"@types/lodash": "^4.14.108", | ||
"@types/mocha": "^5.2.6", | ||
"@types/ncp": "^2.0.1", | ||
"@types/node": "^12.0.2", | ||
"@types/pify": "^3.0.2", | ||
"@types/semver": "^6.0.1", | ||
"clang-format": "^1.0.55", | ||
"execa": "^2.0.3", | ||
"gts": "^1.0.0", | ||
"gulp": "^4.0.2", | ||
"gulp-mocha": "^6.0.0", | ||
"lodash": "^4.17.4", | ||
"typescript": "~3.5.1" | ||
"mocha-jenkins-reporter": "^0.4.1", | ||
"ncp": "^2.0.0", | ||
"pify": "^4.0.1", | ||
"ts-node": "^8.3.0", | ||
"typescript": "^3.5.3" | ||
}, | ||
@@ -47,3 +59,3 @@ "contributors": [ | ||
"dependencies": { | ||
"semver": "^6.0.0" | ||
"semver": "^6.2.0" | ||
}, | ||
@@ -50,0 +62,0 @@ "files": [ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
275497
75
6582
20
Updatedsemver@^6.2.0