网络是困难的
偶尔,乌鸦的镜像系统没有足够的光线来传输信号,或者有些东西阻挡了信号的路径。 信号可能发送了,但从未收到。
事实上,这只会导致提供给send
的回调永远不会被调用,这可能会导致程序停止,而不会注意到问题。 如果在没有得到回应的特定时间段内,请求会超时并报告故障,那就很好。
通常情况下,传输故障是随机事故,例如汽车的前灯会干扰光信号,只需重试请求就可以使其成功。 所以,当我们处理它时,让我们的请求函数在放弃之前自动重试发送请求几次。
而且,既然我们已经确定Promise
是一件好事,我们也会让我们的请求函数返回一个Promise
。 对于他们可以表达的内容,回调和Promise
是等同的。 基于回调的函数可以打包,来公开基于Promise
的接口,反之亦然。
即使请求及其响应已成功传递,响应也可能表明失败 - 例如,如果请求尝试使用未定义的请求类型或处理器,会引发错误。 为了支持这个,send
和defineRequestType
遵循前面提到的惯例,其中传递给回调的第一个参数是故障原因,如果有的话,第二个参数是实际结果。
这些可以由我们的包装翻译成Promise
的解析和拒绝。
class Timeout extends Error {}
function request(nest, target, type, content) {
return new Promise((resolve, reject) => {
let done = false;
function attempt(n) {
nest.send(target, type, content, (failed, value) => {
done = true;
if (failed) reject(failed);
else resolve(value);
});
setTimeout(() => {
if (done) return;
else if (n < 3) attempt(n + 1);
else reject(new Timeout("Timed out"));
}, 250);
}
attempt(1);
});
}
因为Promise
只能解析(或拒绝)一次,所以这个是有效的。 第一次调用resolve
或reject
会决定Promise
的结果,并且任何进一步的调用(例如请求结束后到达的超时,或在另一个请求结束后返回的请求)都将被忽略。
为了构建异步循环,对于重试,我们需要使用递归函数 - 常规循环不允许我们停止并等待异步操作。 attempt
函数尝试发送请求一次。 它还设置了超时,如果 250 毫秒后没有响应返回,则开始下一次尝试,或者如果这是第四次尝试,则以Timeout
实例为理由拒绝该Promise
。
每四分之一秒重试一次,一秒钟后没有响应就放弃,这绝对是任意的。 甚至有可能,如果请求确实过来了,但处理器花费了更长时间,请求将被多次传递。 我们会编写我们的处理器,并记住这个问题 - 重复的消息应该是无害的。
总的来说,我们现在不会建立一个世界级的,强大的网络。 但没关系 - 在计算方面,乌鸦没有很高的预期。
为了完全隔离我们自己的回调,我们将继续,并为defineRequestType
定义一个包装器,它允许处理器返回一个Promise
或明确的值,并且连接到我们的回调。
function requestType(name, handler) {
defineRequestType(name, (nest, content, source,
callback) => {
try {
Promise.resolve(handler(nest, content, source))
.then(response => callback(null, response),
failure => callback(failure));
} catch (exception) {
callback(exception);
}
});
}
如果处理器返回的值还不是Promise
,Promise.resolve
用于将转换为Promise
。
请注意,处理器的调用必须包装在try
块中,以确保直接引发的任何异常都会被提供给回调函数。 这很好地说明了使用原始回调正确处理错误的难度 - 很容易忘记正确处理类似的异常,如果不这样做,故障将无法报告给正确的回调。Promise
使其大部分是自动的,因此不易出错。