|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.ResolvingCall = void 0; |
|
const call_credentials_1 = require("./call-credentials"); |
|
const constants_1 = require("./constants"); |
|
const deadline_1 = require("./deadline"); |
|
const metadata_1 = require("./metadata"); |
|
const logging = require("./logging"); |
|
const control_plane_status_1 = require("./control-plane-status"); |
|
const TRACER_NAME = 'resolving_call'; |
|
class ResolvingCall { |
|
constructor(channel, method, options, filterStackFactory, callNumber) { |
|
this.channel = channel; |
|
this.method = method; |
|
this.filterStackFactory = filterStackFactory; |
|
this.callNumber = callNumber; |
|
this.child = null; |
|
this.readPending = false; |
|
this.pendingMessage = null; |
|
this.pendingHalfClose = false; |
|
this.ended = false; |
|
this.readFilterPending = false; |
|
this.writeFilterPending = false; |
|
this.pendingChildStatus = null; |
|
this.metadata = null; |
|
this.listener = null; |
|
this.statusWatchers = []; |
|
this.deadlineTimer = setTimeout(() => { }, 0); |
|
this.filterStack = null; |
|
this.deadlineStartTime = null; |
|
this.configReceivedTime = null; |
|
this.childStartTime = null; |
|
|
|
|
|
|
|
|
|
|
|
this.credentials = call_credentials_1.CallCredentials.createEmpty(); |
|
this.deadline = options.deadline; |
|
this.host = options.host; |
|
if (options.parentCall) { |
|
if (options.flags & constants_1.Propagate.CANCELLATION) { |
|
options.parentCall.on('cancelled', () => { |
|
this.cancelWithStatus(constants_1.Status.CANCELLED, 'Cancelled by parent call'); |
|
}); |
|
} |
|
if (options.flags & constants_1.Propagate.DEADLINE) { |
|
this.trace('Propagating deadline from parent: ' + |
|
options.parentCall.getDeadline()); |
|
this.deadline = (0, deadline_1.minDeadline)(this.deadline, options.parentCall.getDeadline()); |
|
} |
|
} |
|
this.trace('Created'); |
|
this.runDeadlineTimer(); |
|
} |
|
trace(text) { |
|
logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, '[' + this.callNumber + '] ' + text); |
|
} |
|
runDeadlineTimer() { |
|
clearTimeout(this.deadlineTimer); |
|
this.deadlineStartTime = new Date(); |
|
this.trace('Deadline: ' + (0, deadline_1.deadlineToString)(this.deadline)); |
|
const timeout = (0, deadline_1.getRelativeTimeout)(this.deadline); |
|
if (timeout !== Infinity) { |
|
this.trace('Deadline will be reached in ' + timeout + 'ms'); |
|
const handleDeadline = () => { |
|
if (!this.deadlineStartTime) { |
|
this.cancelWithStatus(constants_1.Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); |
|
return; |
|
} |
|
const deadlineInfo = []; |
|
const deadlineEndTime = new Date(); |
|
deadlineInfo.push(`Deadline exceeded after ${(0, deadline_1.formatDateDifference)(this.deadlineStartTime, deadlineEndTime)}`); |
|
if (this.configReceivedTime) { |
|
if (this.configReceivedTime > this.deadlineStartTime) { |
|
deadlineInfo.push(`name resolution: ${(0, deadline_1.formatDateDifference)(this.deadlineStartTime, this.configReceivedTime)}`); |
|
} |
|
if (this.childStartTime) { |
|
if (this.childStartTime > this.configReceivedTime) { |
|
deadlineInfo.push(`metadata filters: ${(0, deadline_1.formatDateDifference)(this.configReceivedTime, this.childStartTime)}`); |
|
} |
|
} |
|
else { |
|
deadlineInfo.push('waiting for metadata filters'); |
|
} |
|
} |
|
else { |
|
deadlineInfo.push('waiting for name resolution'); |
|
} |
|
if (this.child) { |
|
deadlineInfo.push(...this.child.getDeadlineInfo()); |
|
} |
|
this.cancelWithStatus(constants_1.Status.DEADLINE_EXCEEDED, deadlineInfo.join(',')); |
|
}; |
|
if (timeout <= 0) { |
|
process.nextTick(handleDeadline); |
|
} |
|
else { |
|
this.deadlineTimer = setTimeout(handleDeadline, timeout); |
|
} |
|
} |
|
} |
|
outputStatus(status) { |
|
if (!this.ended) { |
|
this.ended = true; |
|
if (!this.filterStack) { |
|
this.filterStack = this.filterStackFactory.createFilter(); |
|
} |
|
clearTimeout(this.deadlineTimer); |
|
const filteredStatus = this.filterStack.receiveTrailers(status); |
|
this.trace('ended with status: code=' + |
|
filteredStatus.code + |
|
' details="' + |
|
filteredStatus.details + |
|
'"'); |
|
this.statusWatchers.forEach(watcher => watcher(filteredStatus)); |
|
process.nextTick(() => { |
|
var _a; |
|
(_a = this.listener) === null || _a === void 0 ? void 0 : _a.onReceiveStatus(filteredStatus); |
|
}); |
|
} |
|
} |
|
sendMessageOnChild(context, message) { |
|
if (!this.child) { |
|
throw new Error('sendMessageonChild called with child not populated'); |
|
} |
|
const child = this.child; |
|
this.writeFilterPending = true; |
|
this.filterStack.sendMessage(Promise.resolve({ message: message, flags: context.flags })).then(filteredMessage => { |
|
this.writeFilterPending = false; |
|
child.sendMessageWithContext(context, filteredMessage.message); |
|
if (this.pendingHalfClose) { |
|
child.halfClose(); |
|
} |
|
}, (status) => { |
|
this.cancelWithStatus(status.code, status.details); |
|
}); |
|
} |
|
getConfig() { |
|
if (this.ended) { |
|
return; |
|
} |
|
if (!this.metadata || !this.listener) { |
|
throw new Error('getConfig called before start'); |
|
} |
|
const configResult = this.channel.getConfig(this.method, this.metadata); |
|
if (configResult.type === 'NONE') { |
|
this.channel.queueCallForConfig(this); |
|
return; |
|
} |
|
else if (configResult.type === 'ERROR') { |
|
if (this.metadata.getOptions().waitForReady) { |
|
this.channel.queueCallForConfig(this); |
|
} |
|
else { |
|
this.outputStatus(configResult.error); |
|
} |
|
return; |
|
} |
|
|
|
this.configReceivedTime = new Date(); |
|
const config = configResult.config; |
|
if (config.status !== constants_1.Status.OK) { |
|
const { code, details } = (0, control_plane_status_1.restrictControlPlaneStatusCode)(config.status, 'Failed to route call to method ' + this.method); |
|
this.outputStatus({ |
|
code: code, |
|
details: details, |
|
metadata: new metadata_1.Metadata(), |
|
}); |
|
return; |
|
} |
|
if (config.methodConfig.timeout) { |
|
const configDeadline = new Date(); |
|
configDeadline.setSeconds(configDeadline.getSeconds() + config.methodConfig.timeout.seconds); |
|
configDeadline.setMilliseconds(configDeadline.getMilliseconds() + |
|
config.methodConfig.timeout.nanos / 1000000); |
|
this.deadline = (0, deadline_1.minDeadline)(this.deadline, configDeadline); |
|
this.runDeadlineTimer(); |
|
} |
|
this.filterStackFactory.push(config.dynamicFilterFactories); |
|
this.filterStack = this.filterStackFactory.createFilter(); |
|
this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then(filteredMetadata => { |
|
this.child = this.channel.createRetryingCall(config, this.method, this.host, this.credentials, this.deadline); |
|
this.trace('Created child [' + this.child.getCallNumber() + ']'); |
|
this.childStartTime = new Date(); |
|
this.child.start(filteredMetadata, { |
|
onReceiveMetadata: metadata => { |
|
this.trace('Received metadata'); |
|
this.listener.onReceiveMetadata(this.filterStack.receiveMetadata(metadata)); |
|
}, |
|
onReceiveMessage: message => { |
|
this.trace('Received message'); |
|
this.readFilterPending = true; |
|
this.filterStack.receiveMessage(message).then(filteredMesssage => { |
|
this.trace('Finished filtering received message'); |
|
this.readFilterPending = false; |
|
this.listener.onReceiveMessage(filteredMesssage); |
|
if (this.pendingChildStatus) { |
|
this.outputStatus(this.pendingChildStatus); |
|
} |
|
}, (status) => { |
|
this.cancelWithStatus(status.code, status.details); |
|
}); |
|
}, |
|
onReceiveStatus: status => { |
|
this.trace('Received status'); |
|
if (this.readFilterPending) { |
|
this.pendingChildStatus = status; |
|
} |
|
else { |
|
this.outputStatus(status); |
|
} |
|
}, |
|
}); |
|
if (this.readPending) { |
|
this.child.startRead(); |
|
} |
|
if (this.pendingMessage) { |
|
this.sendMessageOnChild(this.pendingMessage.context, this.pendingMessage.message); |
|
} |
|
else if (this.pendingHalfClose) { |
|
this.child.halfClose(); |
|
} |
|
}, (status) => { |
|
this.outputStatus(status); |
|
}); |
|
} |
|
reportResolverError(status) { |
|
var _a; |
|
if ((_a = this.metadata) === null || _a === void 0 ? void 0 : _a.getOptions().waitForReady) { |
|
this.channel.queueCallForConfig(this); |
|
} |
|
else { |
|
this.outputStatus(status); |
|
} |
|
} |
|
cancelWithStatus(status, details) { |
|
var _a; |
|
this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); |
|
(_a = this.child) === null || _a === void 0 ? void 0 : _a.cancelWithStatus(status, details); |
|
this.outputStatus({ |
|
code: status, |
|
details: details, |
|
metadata: new metadata_1.Metadata(), |
|
}); |
|
} |
|
getPeer() { |
|
var _a, _b; |
|
return (_b = (_a = this.child) === null || _a === void 0 ? void 0 : _a.getPeer()) !== null && _b !== void 0 ? _b : this.channel.getTarget(); |
|
} |
|
start(metadata, listener) { |
|
this.trace('start called'); |
|
this.metadata = metadata.clone(); |
|
this.listener = listener; |
|
this.getConfig(); |
|
} |
|
sendMessageWithContext(context, message) { |
|
this.trace('write() called with message of length ' + message.length); |
|
if (this.child) { |
|
this.sendMessageOnChild(context, message); |
|
} |
|
else { |
|
this.pendingMessage = { context, message }; |
|
} |
|
} |
|
startRead() { |
|
this.trace('startRead called'); |
|
if (this.child) { |
|
this.child.startRead(); |
|
} |
|
else { |
|
this.readPending = true; |
|
} |
|
} |
|
halfClose() { |
|
this.trace('halfClose called'); |
|
if (this.child && !this.writeFilterPending) { |
|
this.child.halfClose(); |
|
} |
|
else { |
|
this.pendingHalfClose = true; |
|
} |
|
} |
|
setCredentials(credentials) { |
|
this.credentials = credentials; |
|
} |
|
addStatusWatcher(watcher) { |
|
this.statusWatchers.push(watcher); |
|
} |
|
getCallNumber() { |
|
return this.callNumber; |
|
} |
|
} |
|
exports.ResolvingCall = ResolvingCall; |
|
|