Lint and format

This commit is contained in:
murgatroid99 2017-08-29 10:27:42 -07:00
parent f012088ecc
commit a3c1136fb4
16 changed files with 489 additions and 479 deletions

View File

@ -1,12 +1,12 @@
import {promisify} from 'util' import {promisify} from 'util';
import {Filter, BaseFilter, FilterFactory} from './filter'
import {CallCredentials} from './call-credentials' import {CallCredentials} from './call-credentials';
import {Http2Channel} from './channel' import {CallStream} from './call-stream';
import {CallStream} from './call-stream' import {Http2Channel} from './channel';
import {Metadata} from './metadata' import {BaseFilter, Filter, FilterFactory} from './filter';
import {Metadata} from './metadata';
export class CallCredentialsFilter extends BaseFilter implements Filter { export class CallCredentialsFilter extends BaseFilter implements Filter {
constructor(private readonly credentials: CallCredentials) { constructor(private readonly credentials: CallCredentials) {
super(); super();
} }
@ -20,13 +20,15 @@ export class CallCredentialsFilter extends BaseFilter implements Filter {
} }
} }
export class CallCredentialsFilterFactory implements FilterFactory<CallCredentialsFilter> { export class CallCredentialsFilterFactory implements
FilterFactory<CallCredentialsFilter> {
private readonly credentials: CallCredentials; private readonly credentials: CallCredentials;
constructor(channel: Http2Channel) { constructor(channel: Http2Channel) {
this.credentials = channel.credentials.getCallCredentials(); this.credentials = channel.credentials.getCallCredentials();
} }
createFilter(callStream: CallStream): CallCredentialsFilter { createFilter(callStream: CallStream): CallCredentialsFilter {
return new CallCredentialsFilter(this.credentials.compose(callStream.getCredentials())); return new CallCredentialsFilter(
this.credentials.compose(callStream.getCredentials()));
} }
} }

View File

@ -1,10 +1,10 @@
import { Metadata } from './metadata'; import {map, reduce} from 'lodash';
import {map, reduce} from 'lodash'
export type CallMetadataGenerator = ( import {Metadata} from './metadata';
options: Object,
cb: (err: Error | null, metadata?: Metadata) => void export type CallMetadataGenerator =
) => void; (options: Object, cb: (err: Error|null, metadata?: Metadata) => void) =>
void;
/** /**
* A class that represents a generic method of adding authentication-related * A class that represents a generic method of adding authentication-related
@ -25,32 +25,13 @@ export interface CallCredentials {
compose(callCredentials: CallCredentials): CallCredentials; compose(callCredentials: CallCredentials): CallCredentials;
} }
export namespace CallCredentials {
/**
* Creates a new CallCredentials object from a given function that generates
* Metadata objects.
* @param metadataGenerator A function that accepts a set of options, and
* generates a Metadata object based on these options, which is passed back
* to the caller via a supplied (err, metadata) callback.
*/
export function createFromMetadataGenerator(
metadataGenerator: CallMetadataGenerator
): CallCredentials {
return new SingleCallCredentials(metadataGenerator);
}
export function createEmpty(): CallCredentials {
return new EmptyCallCredentials();
}
}
class ComposedCallCredentials implements CallCredentials { class ComposedCallCredentials implements CallCredentials {
constructor(private creds: CallCredentials[]) {} constructor(private creds: CallCredentials[]) {}
async generateMetadata(options: Object): Promise<Metadata> { async generateMetadata(options: Object): Promise<Metadata> {
let base: Metadata = new Metadata(); let base: Metadata = new Metadata();
let generated: Metadata[] = await Promise.all(map( let generated: Metadata[] = await Promise.all(
this.creds, (cred) => cred.generateMetadata(options))); map(this.creds, (cred) => cred.generateMetadata(options)));
for (let gen of generated) { for (let gen of generated) {
base.merge(gen); base.merge(gen);
} }
@ -83,8 +64,6 @@ class SingleCallCredentials implements CallCredentials{
} }
class EmptyCallCredentials implements CallCredentials { class EmptyCallCredentials implements CallCredentials {
constructor () {}
async generateMetadata(options: Object): Promise<Metadata> { async generateMetadata(options: Object): Promise<Metadata> {
return new Metadata(); return new Metadata();
} }
@ -93,3 +72,21 @@ class EmptyCallCredentials implements CallCredentials {
return other; return other;
} }
} }
export namespace CallCredentials {
/**
* Creates a new CallCredentials object from a given function that generates
* Metadata objects.
* @param metadataGenerator A function that accepts a set of options, and
* generates a Metadata object based on these options, which is passed back
* to the caller via a supplied (err, metadata) callback.
*/
export function createFromMetadataGenerator(
metadataGenerator: CallMetadataGenerator): CallCredentials {
return new SingleCallCredentials(metadataGenerator);
}
export function createEmpty(): CallCredentials {
return new EmptyCallCredentials();
}
}

View File

@ -1,18 +1,14 @@
import * as stream from 'stream';
import * as http2 from 'http2'; import * as http2 from 'http2';
import {Duplex} from 'stream';
import {CallCredentials} from './call-credentials'; import {CallCredentials} from './call-credentials';
import {Status} from './constants'; import {Status} from './constants';
import {Filter} from './filter';
import {FilterStackFactory} from './filter-stack';
import {Metadata} from './metadata'; import {Metadata} from './metadata';
import {ObjectDuplex} from './object-stream'; import {ObjectDuplex} from './object-stream';
import {Filter} from './filter'
import {FilterStackFactory} from './filter-stack';
const { const {HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE} = http2.constants;
HTTP2_HEADER_STATUS,
HTTP2_HEADER_CONTENT_TYPE
} = http2.constants;
export type Deadline = Date | number; export type Deadline = Date | number;
@ -85,21 +81,20 @@ enum ReadState {
READING_MESSAGE READING_MESSAGE
} }
export class Http2CallStream extends stream.Duplex implements CallStream { export class Http2CallStream extends Duplex implements CallStream {
public filterStack: Filter; public filterStack: Filter;
private statusEmitted: boolean = false; private statusEmitted = false;
private http2Stream: http2.ClientHttp2Stream|null = null; private http2Stream: http2.ClientHttp2Stream|null = null;
private pendingRead: boolean = false; private pendingRead = false;
private pendingWrite: Buffer|null = null; private pendingWrite: Buffer|null = null;
private pendingWriteCallback: Function|null = null; private pendingWriteCallback: Function|null = null;
private pendingFinalCallback: Function|null = null; private pendingFinalCallback: Function|null = null;
private readState: ReadState = ReadState.NO_DATA; private readState: ReadState = ReadState.NO_DATA;
private readCompressFlag: boolean = false; private readCompressFlag = false;
private readPartialSize: Buffer = Buffer.alloc(4); private readPartialSize: Buffer = Buffer.alloc(4);
private readSizeRemaining: number = 4; private readSizeRemaining = 4;
private readMessageSize: number = 0; private readMessageSize = 0;
private readPartialMessage: Buffer[] = []; private readPartialMessage: Buffer[] = [];
private readMessageRemaining = 0; private readMessageRemaining = 0;
@ -111,7 +106,8 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
// This is populated (non-null) if and only if the call has ended // This is populated (non-null) if and only if the call has ended
private finalStatus: StatusObject|null = null; private finalStatus: StatusObject|null = null;
constructor(private readonly methodName: string, constructor(
private readonly methodName: string,
private readonly options: CallStreamOptions, private readonly options: CallStreamOptions,
filterStackFactory: FilterStackFactory) { filterStackFactory: FilterStackFactory) {
super({objectMode: true}); super({objectMode: true});
@ -163,9 +159,12 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
this.cancelWithStatus(Status.UNKNOWN, e.message); this.cancelWithStatus(Status.UNKNOWN, e.message);
return; return;
} }
this.filterStack.receiveMetadata(Promise.resolve(metadata)).then((finalMetadata) => { this.filterStack.receiveMetadata(Promise.resolve(metadata))
.then(
(finalMetadata) => {
this.emit('metadata', finalMetadata); this.emit('metadata', finalMetadata);
}, (error) => { },
(error) => {
this.cancelWithStatus(Status.UNKNOWN, error.message); this.cancelWithStatus(Status.UNKNOWN, error.message);
}); });
}); });
@ -180,7 +179,7 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
} }
delete headers['grpc-status']; delete headers['grpc-status'];
} }
let details: string = ''; let details = '';
if (headers.hasOwnProperty('grpc-message')) { if (headers.hasOwnProperty('grpc-message')) {
details = decodeURI(headers['grpc-message']); details = decodeURI(headers['grpc-message']);
} }
@ -191,9 +190,12 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
metadata = new Metadata(); metadata = new Metadata();
} }
let status: StatusObject = {code, details, metadata}; let status: StatusObject = {code, details, metadata};
this.filterStack.receiveTrailers(Promise.resolve(status)).then((finalStatus) => { this.filterStack.receiveTrailers(Promise.resolve(status))
.then(
(finalStatus) => {
this.endCall(finalStatus); this.endCall(finalStatus);
}, (error) => { },
(error) => {
this.endCall({ this.endCall({
code: Status.INTERNAL, code: Status.INTERNAL,
details: 'Failed to process received status', details: 'Failed to process received status',
@ -219,7 +221,9 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
break; break;
case ReadState.READING_SIZE: case ReadState.READING_SIZE:
toRead = Math.min(data.length - readHead, this.readSizeRemaining); toRead = Math.min(data.length - readHead, this.readSizeRemaining);
data.copy(this.readPartialSize, 4 - this.readSizeRemaining, readHead, readHead + toRead); data.copy(
this.readPartialSize, 4 - this.readSizeRemaining, readHead,
readHead + toRead);
this.readSizeRemaining -= toRead; this.readSizeRemaining -= toRead;
readHead += toRead; readHead += toRead;
// readSizeRemaining >=0 here // readSizeRemaining >=0 here
@ -230,14 +234,17 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
} }
break; break;
case ReadState.READING_MESSAGE: case ReadState.READING_MESSAGE:
toRead = Math.min(data.length - readHead, this.readMessageRemaining); toRead =
this.readPartialMessage.push(data.slice(readHead, readHead + toRead)); Math.min(data.length - readHead, this.readMessageRemaining);
this.readPartialMessage.push(
data.slice(readHead, readHead + toRead));
this.readMessageRemaining -= toRead; this.readMessageRemaining -= toRead;
readHead += toRead; readHead += toRead;
// readMessageRemaining >=0 here // readMessageRemaining >=0 here
if (this.readMessageRemaining === 0) { if (this.readMessageRemaining === 0) {
// At this point, we have read a full message // At this point, we have read a full message
const messageBytes = Buffer.concat(this.readPartialMessage, this.readMessageSize); const messageBytes = Buffer.concat(
this.readPartialMessage, this.readMessageSize);
// TODO(murgatroid99): Add receive message filters // TODO(murgatroid99): Add receive message filters
if (canPush) { if (canPush) {
if (!this.push(messageBytes)) { if (!this.push(messageBytes)) {
@ -261,7 +268,7 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
}); });
stream.on('streamClosed', (errorCode) => { stream.on('streamClosed', (errorCode) => {
let code: Status; let code: Status;
let details: string = ''; let details = '';
switch (errorCode) { switch (errorCode) {
case http2.constants.NGHTTP2_REFUSED_STREAM: case http2.constants.NGHTTP2_REFUSED_STREAM:
code = Status.UNAVAILABLE; code = Status.UNAVAILABLE;
@ -280,11 +287,7 @@ export class Http2CallStream extends stream.Duplex implements CallStream {
default: default:
code = Status.INTERNAL; code = Status.INTERNAL;
} }
this.endCall({ this.endCall({code: code, details: details, metadata: new Metadata()});
code: code,
details: details,
metadata: new Metadata()
});
}); });
stream.on('error', () => { stream.on('error', () => {
this.endCall({ this.endCall({

View File

@ -1,6 +1,7 @@
import { CallCredentials } from './call-credentials';
import {createSecureContext, SecureContext} from 'tls'; import {createSecureContext, SecureContext} from 'tls';
import {CallCredentials} from './call-credentials';
/** /**
* A class that contains credentials for communicating over a channel, as well * A class that contains credentials for communicating over a channel, as well
* as a set of per-call credentials, which are applied to every method call made * as a set of per-call credentials, which are applied to every method call made
@ -28,39 +29,6 @@ export interface ChannelCredentials {
getSecureContext(): SecureContext|null; getSecureContext(): SecureContext|null;
} }
export namespace ChannelCredentials {
/**
* Return a new ChannelCredentials instance with a given set of credentials.
* The resulting instance can be used to construct a Channel that communicates
* over TLS.
* @param rootCerts The root certificate data.
* @param privateKey The client certificate private key, if available.
* @param certChain The client certificate key chain, if available.
*/
export function createSsl(rootCerts?: Buffer | null, privateKey?: Buffer | null, certChain?: Buffer | null) : ChannelCredentials {
if (privateKey && !certChain) {
throw new Error('Private key must be given with accompanying certificate chain');
}
if (!privateKey && certChain) {
throw new Error('Certificate chain must be given with accompanying private key');
}
const secureContext = createSecureContext({
ca: rootCerts || undefined,
key: privateKey || undefined,
cert: certChain || undefined
});
return new SecureChannelCredentialsImpl(secureContext);
}
/**
* Return a new ChannelCredentials instance with no credentials.
*/
export function createInsecure() : ChannelCredentials {
return new InsecureChannelCredentialsImpl();
}
}
abstract class ChannelCredentialsImpl implements ChannelCredentials { abstract class ChannelCredentialsImpl implements ChannelCredentials {
protected callCredentials: CallCredentials; protected callCredentials: CallCredentials;
@ -83,7 +51,7 @@ class InsecureChannelCredentialsImpl extends ChannelCredentialsImpl {
} }
compose(callCredentials: CallCredentials): ChannelCredentialsImpl { compose(callCredentials: CallCredentials): ChannelCredentialsImpl {
throw new Error("Cannot compose insecure credentials"); throw new Error('Cannot compose insecure credentials');
} }
getSecureContext(): SecureContext|null { getSecureContext(): SecureContext|null {
@ -94,21 +62,55 @@ class InsecureChannelCredentialsImpl extends ChannelCredentialsImpl {
class SecureChannelCredentialsImpl extends ChannelCredentialsImpl { class SecureChannelCredentialsImpl extends ChannelCredentialsImpl {
secureContext: SecureContext; secureContext: SecureContext;
constructor( constructor(secureContext: SecureContext, callCredentials?: CallCredentials) {
secureContext: SecureContext,
callCredentials?: CallCredentials
) {
super(callCredentials); super(callCredentials);
this.secureContext = secureContext; this.secureContext = secureContext;
} }
compose(callCredentials: CallCredentials): ChannelCredentialsImpl { compose(callCredentials: CallCredentials): ChannelCredentialsImpl {
const combinedCallCredentials = this.callCredentials.compose(callCredentials); const combinedCallCredentials =
return new SecureChannelCredentialsImpl(this.secureContext, this.callCredentials.compose(callCredentials);
combinedCallCredentials); return new SecureChannelCredentialsImpl(
this.secureContext, combinedCallCredentials);
} }
getSecureContext(): SecureContext|null { getSecureContext(): SecureContext|null {
return this.secureContext; return this.secureContext;
} }
} }
export namespace ChannelCredentials {
/**
* Return a new ChannelCredentials instance with a given set of credentials.
* The resulting instance can be used to construct a Channel that communicates
* over TLS.
* @param rootCerts The root certificate data.
* @param privateKey The client certificate private key, if available.
* @param certChain The client certificate key chain, if available.
*/
export function createSsl(
rootCerts?: Buffer|null, privateKey?: Buffer|null,
certChain?: Buffer|null): ChannelCredentials {
if (privateKey && !certChain) {
throw new Error(
'Private key must be given with accompanying certificate chain');
}
if (!privateKey && certChain) {
throw new Error(
'Certificate chain must be given with accompanying private key');
}
const secureContext = createSecureContext({
ca: rootCerts || undefined,
key: privateKey || undefined,
cert: certChain || undefined
});
return new SecureChannelCredentialsImpl(secureContext);
}
/**
* Return a new ChannelCredentials instance with no credentials.
*/
export function createInsecure(): ChannelCredentials {
return new InsecureChannelCredentialsImpl();
}
}

View File

@ -1,17 +1,17 @@
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {SecureContext} from 'tls';
import * as http2 from 'http2'; import * as http2 from 'http2';
import {SecureContext} from 'tls';
import * as url from 'url'; import * as url from 'url';
import {CallOptions, CallStreamOptions, CallStream, Http2CallStream} from './call-stream';
import {CallCredentials} from './call-credentials';
import {ChannelCredentials} from './channel-credentials';
import {Metadata, MetadataObject} from './metadata';
import {Status} from './constants'
import {FilterStackFactory} from './filter-stack' import {CallCredentials} from './call-credentials';
import {DeadlineFilterFactory} from './deadline-filter' import {CallCredentialsFilterFactory} from './call-credentials-filter';
import {CallCredentialsFilterFactory} from './call-credentials-filter' import {CallOptions, CallStream, CallStreamOptions, Http2CallStream} from './call-stream';
import {CompressionFilterFactory} from './compression-filter' import {ChannelCredentials} from './channel-credentials';
import {CompressionFilterFactory} from './compression-filter';
import {Status} from './constants';
import {DeadlineFilterFactory} from './deadline-filter';
import {FilterStackFactory} from './filter-stack';
import {Metadata, MetadataObject} from './metadata';
const IDLE_TIMEOUT_MS = 300000; const IDLE_TIMEOUT_MS = 300000;
@ -45,7 +45,8 @@ export enum ConnectivityState {
* by a given address. * by a given address.
*/ */
export interface Channel extends EventEmitter { export interface Channel extends EventEmitter {
createStream(methodName: string, metadata: Metadata, options: CallOptions): CallStream; createStream(methodName: string, metadata: Metadata, options: CallOptions):
CallStream;
connect(callback: () => void): void; connect(callback: () => void): void;
getConnectivityState(): ConnectivityState; getConnectivityState(): ConnectivityState;
close(): void; close(): void;
@ -94,7 +95,7 @@ export class Http2Channel extends EventEmitter implements Channel {
private goIdle(): void { private goIdle(): void {
if (this.subChannel !== null) { if (this.subChannel !== null) {
this.subChannel.shutdown({graceful: true}, () => {}); this.subChannel.shutdown({graceful: true}, () => undefined);
this.subChannel = null; this.subChannel = null;
} }
this.transitionToState(ConnectivityState.IDLE); this.transitionToState(ConnectivityState.IDLE);
@ -106,7 +107,8 @@ export class Http2Channel extends EventEmitter implements Channel {
} }
} }
constructor(private readonly address: url.URL, constructor(
private readonly address: url.URL,
public readonly credentials: ChannelCredentials, public readonly credentials: ChannelCredentials,
private readonly options: ChannelOptions) { private readonly options: ChannelOptions) {
super(); super();
@ -117,15 +119,17 @@ export class Http2Channel extends EventEmitter implements Channel {
} }
this.filterStackFactory = new FilterStackFactory([ this.filterStackFactory = new FilterStackFactory([
new CompressionFilterFactory(this), new CompressionFilterFactory(this),
new CallCredentialsFilterFactory(this), new CallCredentialsFilterFactory(this), new DeadlineFilterFactory(this)
new DeadlineFilterFactory(this)
]); ]);
} }
private startHttp2Stream(methodName: string, stream: Http2CallStream, metadata: Metadata) { private startHttp2Stream(
let finalMetadata: Promise<Metadata> = stream.filterStack.sendMetadata(Promise.resolve(metadata)); methodName: string, stream: Http2CallStream, metadata: Metadata) {
let finalMetadata: Promise<Metadata> =
stream.filterStack.sendMetadata(Promise.resolve(metadata));
this.connect(() => { this.connect(() => {
finalMetadata.then((metadataValue) => { finalMetadata.then(
(metadataValue) => {
let headers = metadataValue.toHttp2Headers(); let headers = metadataValue.toHttp2Headers();
headers[HTTP2_HEADER_AUTHORITY] = this.address.hostname; headers[HTTP2_HEADER_AUTHORITY] = this.address.hostname;
headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc';
@ -138,20 +142,23 @@ export class Http2Channel extends EventEmitter implements Channel {
(this.subChannel as http2.ClientHttp2Session); (this.subChannel as http2.ClientHttp2Session);
stream.attachHttp2Stream(session.request(headers)); stream.attachHttp2Stream(session.request(headers));
} else { } else {
/* In this case, we lost the connection while finalizing metadata. /* In this case, we lost the connection while finalizing
* That should be very unusual */ * metadata. That should be very unusual */
setImmediate(() => { setImmediate(() => {
this.startHttp2Stream(methodName, stream, metadata); this.startHttp2Stream(methodName, stream, metadata);
}); });
} }
} }
}, (error) => { },
stream.cancelWithStatus(Status.UNKNOWN, "Failed to generate metadata"); (error) => {
stream.cancelWithStatus(
Status.UNKNOWN, 'Failed to generate metadata');
}); });
}); });
} }
createStream(methodName: string, metadata: Metadata, options: CallOptions): CallStream { createStream(methodName: string, metadata: Metadata, options: CallOptions):
CallStream {
if (this.connectivityState === ConnectivityState.SHUTDOWN) { if (this.connectivityState === ConnectivityState.SHUTDOWN) {
throw new Error('Channel has been shut down'); throw new Error('Channel has been shut down');
} }
@ -159,8 +166,9 @@ export class Http2Channel extends EventEmitter implements Channel {
deadline: options.deadline === undefined ? Infinity : options.deadline, deadline: options.deadline === undefined ? Infinity : options.deadline,
credentials: options.credentials || CallCredentials.createEmpty(), credentials: options.credentials || CallCredentials.createEmpty(),
flags: options.flags || 0 flags: options.flags || 0
} };
let stream: Http2CallStream = new Http2CallStream(methodName, finalOptions, this.filterStackFactory); let stream: Http2CallStream =
new Http2CallStream(methodName, finalOptions, this.filterStackFactory);
this.startHttp2Stream(methodName, stream, metadata); this.startHttp2Stream(methodName, stream, metadata);
return stream; return stream;
} }

View File

@ -3,7 +3,7 @@ import {URL} from 'url';
import {ClientDuplexStream, ClientDuplexStreamImpl, ClientReadableStream, ClientReadableStreamImpl, ClientUnaryCall, ClientUnaryCallImpl, ClientWritableStream, ClientWritableStreamImpl, ServiceError, ServiceErrorImpl} from './call'; import {ClientDuplexStream, ClientDuplexStreamImpl, ClientReadableStream, ClientReadableStreamImpl, ClientUnaryCall, ClientUnaryCallImpl, ClientWritableStream, ClientWritableStreamImpl, ServiceError, ServiceErrorImpl} from './call';
import {CallOptions, CallStream, StatusObject, WriteObject} from './call-stream'; import {CallOptions, CallStream, StatusObject, WriteObject} from './call-stream';
import {Channel, Http2Channel, ChannelOptions} from './channel'; import {Channel, ChannelOptions, Http2Channel} from './channel';
import {ChannelCredentials} from './channel-credentials'; import {ChannelCredentials} from './channel-credentials';
import {Status} from './constants'; import {Status} from './constants';
import {Metadata} from './metadata'; import {Metadata} from './metadata';
@ -34,11 +34,11 @@ export class Client {
waitForReady(deadline: Date|number, callback: (error: Error|null) => void): waitForReady(deadline: Date|number, callback: (error: Error|null) => void):
void { void {
let cb: (error: Error|null) => void = once(callback); let cb: (error: Error|null) => void = once(callback);
let callbackCalled: boolean = false; let callbackCalled = false;
this.channel.connect(() => { this.channel.connect(() => {
cb(null); cb(null);
}); });
if (deadline != Infinity) { if (deadline !== Infinity) {
let timeout: number; let timeout: number;
let now: number = (new Date).getTime(); let now: number = (new Date).getTime();
if (deadline instanceof Date) { if (deadline instanceof Date) {

View File

@ -1,10 +1,9 @@
import {CallStream} from './call-stream' import {CallStream} from './call-stream';
import {Channel} from './channel' import {Channel} from './channel';
import {Filter, BaseFilter, FilterFactory} from './filter' import {BaseFilter, Filter, FilterFactory} from './filter';
import {Metadata} from './metadata' import {Metadata} from './metadata';
export class CompressionFilter extends BaseFilter implements Filter { export class CompressionFilter extends BaseFilter implements Filter {
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> { async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
const headers: Metadata = await metadata; const headers: Metadata = await metadata;
headers.set('grpc-encoding', 'identity'); headers.set('grpc-encoding', 'identity');
@ -20,8 +19,9 @@ export class CompressionFilter extends BaseFilter implements Filter {
} }
} }
export class CompressionFilterFactory implements FilterFactory<CompressionFilter> { export class CompressionFilterFactory implements
constructor(channel: Channel) {} FilterFactory<CompressionFilter> {
constructor(private readonly channel: Channel) {}
createFilter(callStream: CallStream): CompressionFilter { createFilter(callStream: CallStream): CompressionFilter {
return new CompressionFilter(); return new CompressionFilter();
} }

View File

@ -1,19 +1,17 @@
import {CallStream} from './call-stream' import {CallStream} from './call-stream';
import {Channel, Http2Channel} from './channel' import {Channel, Http2Channel} from './channel';
import {Filter, BaseFilter, FilterFactory} from './filter' import {Status} from './constants';
import {Status} from './constants' import {BaseFilter, Filter, FilterFactory} from './filter';
import {Metadata} from './metadata' import {Metadata} from './metadata';
const units: [string, number][] = [ const units: [string, number][] =
['m', 1], [['m', 1], ['S', 1000], ['M', 60 * 1000], ['H', 60 * 60 * 1000]];
['S', 1000],
['M', 60 * 1000],
['H', 60 * 60 * 1000]
]
export class DeadlineFilter extends BaseFilter implements Filter { export class DeadlineFilter extends BaseFilter implements Filter {
private deadline: number; private deadline: number;
constructor(private readonly channel: Http2Channel, private readonly callStream: CallStream) { constructor(
private readonly channel: Http2Channel,
private readonly callStream: CallStream) {
super(); super();
let callDeadline = callStream.getDeadline(); let callDeadline = callStream.getDeadline();
if (callDeadline instanceof Date) { if (callDeadline instanceof Date) {
@ -28,7 +26,8 @@ export class DeadlineFilter extends BaseFilter implements Filter {
} }
if (this.deadline !== Infinity) { if (this.deadline !== Infinity) {
setTimeout(() => { setTimeout(() => {
callStream.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); callStream.cancelWithStatus(
Status.DEADLINE_EXCEEDED, 'Deadline exceeded');
}, timeout); }, timeout);
} }
} }
@ -37,7 +36,8 @@ export class DeadlineFilter extends BaseFilter implements Filter {
if (this.deadline === Infinity) { if (this.deadline === Infinity) {
return await metadata; return await metadata;
} }
let timeoutString : Promise<string> = new Promise<string>((resolve, reject) => { let timeoutString: Promise<string> =
new Promise<string>((resolve, reject) => {
this.channel.connect(() => { this.channel.connect(() => {
let now = (new Date()).getTime(); let now = (new Date()).getTime();
let timeoutMs = this.deadline - now; let timeoutMs = this.deadline - now;

View File

@ -1,21 +1,26 @@
import {flow, flowRight, map} from 'lodash'; import {flow, flowRight, map} from 'lodash';
import {Metadata} from './metadata';
import {CallStream, StatusObject} from './call-stream' import {CallStream, StatusObject} from './call-stream';
import {Filter, FilterFactory} from './filter'; import {Filter, FilterFactory} from './filter';
import {Metadata} from './metadata';
export class FilterStack implements Filter { export class FilterStack implements Filter {
constructor(private readonly filters: Filter[]) {} constructor(private readonly filters: Filter[]) {}
sendMetadata(metadata: Promise<Metadata>) { sendMetadata(metadata: Promise<Metadata>) {
return flow(map(this.filters, (filter) => filter.sendMetadata.bind(filter)))(metadata); return flow(map(
this.filters, (filter) => filter.sendMetadata.bind(filter)))(metadata);
} }
receiveMetadata(metadata: Promise<Metadata>) { receiveMetadata(metadata: Promise<Metadata>) {
return flowRight(map(this.filters, (filter) => filter.receiveMetadata.bind(filter)))(metadata); return flowRight(
map(this.filters, (filter) => filter.receiveMetadata.bind(filter)))(
metadata);
} }
receiveTrailers(status: Promise<StatusObject>): Promise<StatusObject> { receiveTrailers(status: Promise<StatusObject>): Promise<StatusObject> {
return flowRight(map(this.filters, (filter) => filter.receiveTrailers.bind(filter)))(status); return flowRight(map(
this.filters, (filter) => filter.receiveTrailers.bind(filter)))(status);
} }
} }
@ -23,6 +28,7 @@ export class FilterStackFactory implements FilterFactory<FilterStack> {
constructor(private readonly factories: FilterFactory<any>[]) {} constructor(private readonly factories: FilterFactory<any>[]) {}
createFilter(callStream: CallStream): FilterStack { createFilter(callStream: CallStream): FilterStack {
return new FilterStack(map(this.factories, (factory) => factory.createFilter(callStream))); return new FilterStack(
map(this.factories, (factory) => factory.createFilter(callStream)));
} }
} }

View File

@ -1,9 +1,10 @@
import {Metadata} from './metadata' import {CallStream, StatusObject} from './call-stream';
import {StatusObject, CallStream} from './call-stream' import {Metadata} from './metadata';
/** /**
* Filter classes represent related per-call logic and state that is primarily * Filter classes represent related per-call logic and state that is primarily
* used to modify incoming and outgoing data */ * used to modify incoming and outgoing data
*/
export interface Filter { export interface Filter {
sendMetadata(metadata: Promise<Metadata>): Promise<Metadata>; sendMetadata(metadata: Promise<Metadata>): Promise<Metadata>;

View File

@ -1,11 +1,9 @@
import { forOwn } from 'lodash';
import * as http2 from 'http2'; import * as http2 from 'http2';
import {forOwn} from 'lodash';
export type MetadataValue = string | Buffer; export type MetadataValue = string | Buffer;
export interface MetadataObject { export interface MetadataObject { [key: string]: Array<MetadataValue>; }
[key: string]: Array<MetadataValue>;
}
function cloneMetadataObject(repr: MetadataObject): MetadataObject { function cloneMetadataObject(repr: MetadataObject): MetadataObject {
const result: MetadataObject = {}; const result: MetadataObject = {};
@ -54,7 +52,8 @@ function validate(key: string, value?: MetadataValue): void {
'keys that don\'t end with \'-bin\' must have String values'); 'keys that don\'t end with \'-bin\' must have String values');
} }
if (!isLegalNonBinaryValue(value)) { if (!isLegalNonBinaryValue(value)) {
throw new Error('Metadata string value "' + value + throw new Error(
'Metadata string value "' + value +
'" contains illegal characters'); '" contains illegal characters');
} }
} }
@ -66,7 +65,6 @@ function validate(key: string, value?: MetadataValue): void {
*/ */
export class Metadata { export class Metadata {
protected internalRepr: MetadataObject = {}; protected internalRepr: MetadataObject = {};
constructor() {}
/** /**
* Sets the given value for the given key by replacing any other values * Sets the given value for the given key by replacing any other values
@ -195,7 +193,7 @@ export class Metadata {
if (Array.isArray(values)) { if (Array.isArray(values)) {
values.forEach((value) => { values.forEach((value) => {
result.add(key, Buffer.from(value, 'base64')); result.add(key, Buffer.from(value, 'base64'));
}) });
} else { } else {
result.add(key, Buffer.from(values, 'base64')); result.add(key, Buffer.from(values, 'base64'));
} }
@ -203,7 +201,7 @@ export class Metadata {
if (Array.isArray(values)) { if (Array.isArray(values)) {
values.forEach((value) => { values.forEach((value) => {
result.add(key, value); result.add(key, value);
}) });
} else { } else {
result.add(key, values); result.add(key, values);
} }

View File

@ -1,4 +1,4 @@
import { Readable, Writable, Duplex } from 'stream'; import {Duplex, Readable, Writable} from 'stream';
export interface IntermediateObjectReadable<T> extends Readable { export interface IntermediateObjectReadable<T> extends Readable {
read(size?: number): any&T; read(size?: number): any&T;
@ -44,7 +44,8 @@ export interface ObjectWritable<T> extends IntermediateObjectWritable<T> {
end(chunk: T, encoding?: any, cb?: Function): void; end(chunk: T, encoding?: any, cb?: Function): void;
} }
export interface ObjectDuplex<T, U> extends Duplex, ObjectWritable<T>, ObjectReadable<U> { export interface ObjectDuplex<T, U> extends Duplex, ObjectWritable<T>,
ObjectReadable<U> {
read(size?: number): U; read(size?: number): U;
_write(chunk: T, encoding: string, callback: Function): void; _write(chunk: T, encoding: string, callback: Function): void;

View File

@ -1,7 +1,8 @@
import { Metadata } from '../src/metadata';
import { CallCredentials, CallMetadataGenerator } from '../src/call-credentials';
import * as assert from 'assert'; import * as assert from 'assert';
import {CallCredentials, CallMetadataGenerator} from '../src/call-credentials';
import {Metadata} from '../src/metadata';
// Metadata generators // Metadata generators
function makeGenerator(props: Array<string>): CallMetadataGenerator { function makeGenerator(props: Array<string>): CallMetadataGenerator {
@ -13,19 +14,19 @@ function makeGenerator(props: Array<string>): CallMetadataGenerator {
} }
}); });
cb(null, metadata); cb(null, metadata);
} };
} }
function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator { function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator {
return (_options, cb) => { return (options, cb) => {
const metadata = new Metadata(); const metadata = new Metadata();
metadata.add('msElapsed', `${ms}`); metadata.add('msElapsed', `${ms}`);
setTimeout(() => cb(null, metadata), ms); setTimeout(() => cb(null, metadata), ms);
}; };
}; }
const generateFromName: CallMetadataGenerator = makeGenerator(['name']); const generateFromName: CallMetadataGenerator = makeGenerator(['name']);
const generateWithError: CallMetadataGenerator = (_options, cb) => const generateWithError: CallMetadataGenerator = (options, cb) =>
cb(new Error()); cb(new Error());
// Tests // Tests
@ -33,23 +34,27 @@ const generateWithError: CallMetadataGenerator = (_options, cb) =>
describe('CallCredentials', () => { describe('CallCredentials', () => {
describe('createFromMetadataGenerator', () => { describe('createFromMetadataGenerator', () => {
it('should accept a metadata generator', () => { it('should accept a metadata generator', () => {
assert.doesNotThrow(() => assert.doesNotThrow(
CallCredentials.createFromMetadataGenerator(generateFromName)); () => CallCredentials.createFromMetadataGenerator(generateFromName));
}); });
}); });
describe('compose', () => { describe('compose', () => {
it('should accept a CallCredentials object and return a new object', () => { it('should accept a CallCredentials object and return a new object', () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(generateFromName); const callCredentials1 =
const callCredentials2 = CallCredentials.createFromMetadataGenerator(generateFromName); CallCredentials.createFromMetadataGenerator(generateFromName);
const callCredentials2 =
CallCredentials.createFromMetadataGenerator(generateFromName);
const combinedCredentials = callCredentials1.compose(callCredentials2); const combinedCredentials = callCredentials1.compose(callCredentials2);
assert.notEqual(combinedCredentials, callCredentials1); assert.notEqual(combinedCredentials, callCredentials1);
assert.notEqual(combinedCredentials, callCredentials2); assert.notEqual(combinedCredentials, callCredentials2);
}); });
it('should be chainable', () => { it('should be chainable', () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(generateFromName); const callCredentials1 =
const callCredentials2 = CallCredentials.createFromMetadataGenerator(generateFromName); CallCredentials.createFromMetadataGenerator(generateFromName);
const callCredentials2 =
CallCredentials.createFromMetadataGenerator(generateFromName);
assert.doesNotThrow(() => { assert.doesNotThrow(() => {
callCredentials1.compose(callCredentials2) callCredentials1.compose(callCredentials2)
.compose(callCredentials2) .compose(callCredentials2)
@ -61,7 +66,8 @@ describe('CallCredentials', () => {
describe('generateMetadata', () => { describe('generateMetadata', () => {
it('should call the function passed to createFromMetadataGenerator', it('should call the function passed to createFromMetadataGenerator',
async () => { async () => {
const callCredentials = CallCredentials.createFromMetadataGenerator(generateFromName); const callCredentials =
CallCredentials.createFromMetadataGenerator(generateFromName);
let metadata: Metadata; let metadata: Metadata;
try { try {
metadata = await callCredentials.generateMetadata({name: 'foo'}); metadata = await callCredentials.generateMetadata({name: 'foo'});
@ -69,13 +75,12 @@ describe('CallCredentials', () => {
throw err; throw err;
} }
assert.deepEqual(metadata.get('name'), ['foo']); assert.deepEqual(metadata.get('name'), ['foo']);
} });
);
it('should emit an error if the associated metadataGenerator does', it('should emit an error if the associated metadataGenerator does',
async () => { async () => {
const callCredentials = CallCredentials.createFromMetadataGenerator( const callCredentials =
generateWithError); CallCredentials.createFromMetadataGenerator(generateWithError);
let metadata: Metadata|null = null; let metadata: Metadata|null = null;
try { try {
metadata = await callCredentials.generateMetadata({}); metadata = await callCredentials.generateMetadata({});
@ -83,8 +88,7 @@ describe('CallCredentials', () => {
assert.ok(err instanceof Error); assert.ok(err instanceof Error);
} }
assert.strictEqual(metadata, null); assert.strictEqual(metadata, null);
} });
);
it('should combine metadata from multiple generators', async () => { it('should combine metadata from multiple generators', async () => {
const [callCreds1, callCreds2, callCreds3, callCreds4] = const [callCreds1, callCreds2, callCreds3, callCreds4] =
@ -93,23 +97,21 @@ describe('CallCredentials', () => {
makeAfterMsElapsedGenerator(ms); makeAfterMsElapsedGenerator(ms);
return CallCredentials.createFromMetadataGenerator(generator); return CallCredentials.createFromMetadataGenerator(generator);
}); });
const testCases = [{ const testCases = [
credentials: callCreds1 {
.compose(callCreds2) credentials: callCreds1.compose(callCreds2)
.compose(callCreds3) .compose(callCreds3)
.compose(callCreds4), .compose(callCreds4),
expected: ['50', '100', '150', '200'] expected: ['50', '100', '150', '200']
}, { },
credentials: callCreds4 {
.compose(callCreds3 credentials: callCreds4.compose(
.compose(callCreds2 callCreds3.compose(callCreds2.compose(callCreds1))),
.compose(callCreds1))),
expected: ['200', '150', '100', '50'] expected: ['200', '150', '100', '50']
}, { },
credentials: callCreds3 {
.compose(callCreds4 credentials: callCreds3.compose(
.compose(callCreds1) callCreds4.compose(callCreds1).compose(callCreds2)),
.compose(callCreds2)),
expected: ['150', '200', '50', '100'] expected: ['150', '200', '50', '100']
} }
]; ];

View File

@ -1,10 +1,12 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { CallCredentials } from '../src/call-credentials';
import { ChannelCredentials } from '../src/channel-credentials';
import { mockFunction, assert2 } from './common';
import * as fs from 'fs'; import * as fs from 'fs';
import {promisify} from 'util'; import {promisify} from 'util';
import {CallCredentials} from '../src/call-credentials';
import {ChannelCredentials} from '../src/channel-credentials';
import {assert2, mockFunction} from './common';
class CallCredentialsMock implements CallCredentials { class CallCredentialsMock implements CallCredentials {
child: CallCredentialsMock; child: CallCredentialsMock;
constructor(child?: CallCredentialsMock) { constructor(child?: CallCredentialsMock) {
@ -32,22 +34,17 @@ class CallCredentialsMock implements CallCredentials {
const readFile: (...args: any[]) => Promise<Buffer> = promisify(fs.readFile); const readFile: (...args: any[]) => Promise<Buffer> = promisify(fs.readFile);
// A promise which resolves to loaded files in the form { ca, key, cert } // A promise which resolves to loaded files in the form { ca, key, cert }
const pFixtures = Promise.all([ const pFixtures = Promise
'ca.pem', .all(['ca.pem', 'server1.key', 'server1.pem'].map(
'server1.key', (file) => readFile(`test/fixtures/${file}`)))
'server1.pem' .then((result) => {
].map((file) => readFile(`test/fixtures/${file}`)) return {ca: result[0], key: result[1], cert: result[2]};
).then((result) => {
return {
ca: result[0],
key: result[1],
cert: result[2]
};
}); });
describe('ChannelCredentials Implementation', () => { describe('ChannelCredentials Implementation', () => {
describe('createInsecure', () => { describe('createInsecure', () => {
it('should return a ChannelCredentials object with no associated secure context', () => { it('should return a ChannelCredentials object with no associated secure context',
() => {
const creds = assert2.noThrowAndReturn( const creds = assert2.noThrowAndReturn(
() => ChannelCredentials.createInsecure()); () => ChannelCredentials.createInsecure());
assert.ok(!creds.getSecureContext()); assert.ok(!creds.getSecureContext());
@ -56,15 +53,15 @@ describe('ChannelCredentials Implementation', () => {
describe('createSsl', () => { describe('createSsl', () => {
it('should work when given no arguments', () => { it('should work when given no arguments', () => {
const creds: ChannelCredentials = assert2.noThrowAndReturn( const creds: ChannelCredentials =
() => ChannelCredentials.createSsl()); assert2.noThrowAndReturn(() => ChannelCredentials.createSsl());
assert.ok(!!creds.getSecureContext()); assert.ok(!!creds.getSecureContext());
}); });
it('should work with just a CA override', async () => { it('should work with just a CA override', async () => {
const {ca} = await pFixtures; const {ca} = await pFixtures;
const creds = assert2.noThrowAndReturn( const creds =
() => ChannelCredentials.createSsl(ca)); assert2.noThrowAndReturn(() => ChannelCredentials.createSsl(ca));
assert.ok(!!creds.getSecureContext()); assert.ok(!!creds.getSecureContext());
}); });
@ -99,8 +96,7 @@ describe('ChannelCredentials Implementation', () => {
const channelCreds = ChannelCredentials.createSsl(); const channelCreds = ChannelCredentials.createSsl();
const callCreds = new CallCredentialsMock(); const callCreds = new CallCredentialsMock();
const composedChannelCreds = channelCreds.compose(callCreds); const composedChannelCreds = channelCreds.compose(callCreds);
assert.strictEqual(composedChannelCreds.getCallCredentials(), assert.strictEqual(composedChannelCreds.getCallCredentials(), callCreds);
callCreds);
}); });
it('should be chainable', () => { it('should be chainable', () => {

View File

@ -1,31 +1,30 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as http2 from 'http2'; import * as http2 from 'http2';
import {range} from 'lodash'; import {range} from 'lodash';
import * as metadata from '../src/metadata'; import {Metadata} from '../src/metadata';
class Metadata extends metadata.Metadata { class TestMetadata extends Metadata {
getInternalRepresentation() { getInternalRepresentation() {
return this.internalRepr; return this.internalRepr;
} }
static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata { static fromHttp2Headers(headers: http2.IncomingHttpHeaders): TestMetadata {
const result = metadata.Metadata.fromHttp2Headers(headers) as Metadata; const result = Metadata.fromHttp2Headers(headers) as TestMetadata;
result.getInternalRepresentation = result.getInternalRepresentation =
Metadata.prototype.getInternalRepresentation; TestMetadata.prototype.getInternalRepresentation;
return result; return result;
} }
} }
const validKeyChars = '0123456789abcdefghijklmnopqrstuvwxyz_-.'; const validKeyChars = '0123456789abcdefghijklmnopqrstuvwxyz_-.';
const validNonBinValueChars = range(0x20, 0x7f) const validNonBinValueChars =
.map(code => String.fromCharCode(code)) range(0x20, 0x7f).map(code => String.fromCharCode(code)).join('');
.join('');
describe('Metadata', () => { describe('Metadata', () => {
let metadata: Metadata; let metadata: TestMetadata;
beforeEach(() => { beforeEach(() => {
metadata = new Metadata(); metadata = new TestMetadata();
}); });
describe('set', () => { describe('set', () => {
@ -178,10 +177,8 @@ describe('Metadata', () => {
metadata.add('Key2', 'value2'); metadata.add('Key2', 'value2');
metadata.add('KEY3', 'value3a'); metadata.add('KEY3', 'value3a');
metadata.add('KEY3', 'value3b'); metadata.add('KEY3', 'value3b');
assert.deepEqual(metadata.getMap(), assert.deepEqual(
{key1: 'value1', metadata.getMap(), {key1: 'value1', key2: 'value2', key3: 'value3a'});
key2: 'value2',
key3: 'value3a'});
}); });
}); });
@ -213,7 +210,7 @@ describe('Metadata', () => {
metadata.add('Key2', 'value2a'); metadata.add('Key2', 'value2a');
metadata.add('KEY3', 'value3a'); metadata.add('KEY3', 'value3a');
metadata.add('key4', 'value4'); metadata.add('key4', 'value4');
const metadata2 = new Metadata(); const metadata2 = new TestMetadata();
metadata2.add('KEY1', 'value1'); metadata2.add('KEY1', 'value1');
metadata2.add('key2', 'value2b'); metadata2.add('key2', 'value2b');
metadata2.add('key3', 'value3b'); metadata2.add('key3', 'value3b');
@ -246,8 +243,7 @@ describe('Metadata', () => {
key2: ['value2'], key2: ['value2'],
key3: ['value3a', 'value3b'], key3: ['value3a', 'value3b'],
'key-bin': [ 'key-bin': [
'AAECAwQFBgcICQoLDA0ODw==', 'AAECAwQFBgcICQoLDA0ODw==', 'EBESExQVFhcYGRobHB0eHw==',
'EBESExQVFhcYGRobHB0eHw==',
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=' 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
] ]
}); });
@ -265,27 +261,25 @@ describe('Metadata', () => {
key2: ['value2'], key2: ['value2'],
key3: ['value3a', 'value3b'], key3: ['value3a', 'value3b'],
'key-bin': [ 'key-bin': [
'AAECAwQFBgcICQoLDA0ODw==', 'AAECAwQFBgcICQoLDA0ODw==', 'EBESExQVFhcYGRobHB0eHw==',
'EBESExQVFhcYGRobHB0eHw==',
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=' 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
] ]
}; };
const metadataFromHeaders = Metadata.fromHttp2Headers(headers); const metadataFromHeaders = TestMetadata.fromHttp2Headers(headers);
const internalRepr = metadataFromHeaders.getInternalRepresentation(); const internalRepr = metadataFromHeaders.getInternalRepresentation();
assert.deepEqual(internalRepr, { assert.deepEqual(internalRepr, {
key1: ['value1'], key1: ['value1'],
key2: ['value2'], key2: ['value2'],
key3: ['value3a', 'value3b'], key3: ['value3a', 'value3b'],
'key-bin': [ 'key-bin': [
Buffer.from(range(0, 16)), Buffer.from(range(0, 16)), Buffer.from(range(16, 32)),
Buffer.from(range(16, 32)),
Buffer.from(range(0, 32)) Buffer.from(range(0, 32))
] ]
}); });
}); });
it('creates an empty Metadata object from empty headers', () => { it('creates an empty Metadata object from empty headers', () => {
const metadataFromHeaders = Metadata.fromHttp2Headers({}); const metadataFromHeaders = TestMetadata.fromHttp2Headers({});
const internalRepr = metadataFromHeaders.getInternalRepresentation(); const internalRepr = metadataFromHeaders.getInternalRepresentation();
assert.deepEqual(internalRepr, {}); assert.deepEqual(internalRepr, {});
}); });