网络是困难的

偶尔,乌鸦的镜像系统没有足够的光线来传输信号,或者有些东西阻挡了信号的路径。 信号可能发送了,但从未收到。

事实上,这只会导致提供给send的回调永远不会被调用,这可能会导致程序停止,而不会注意到问题。 如果在没有得到回应的特定时间段内,请求会超时并报告故障,那就很好。

通常情况下,传输故障是随机事故,例如汽车的前灯会干扰光信号,只需重试请求就可以使其成功。 所以,当我们处理它时,让我们的请求函数在放弃之前自动重试发送请求几次。

而且,既然我们已经确定Promise是一件好事,我们也会让我们的请求函数返回一个Promise。 对于他们可以表达的内容,回调和Promise是等同的。 基于回调的函数可以打包,来公开基于Promise的接口,反之亦然。

即使请求及其响应已成功传递,响应也可能表明失败 - 例如,如果请求尝试使用未定义的请求类型或处理器,会引发错误。 为了支持这个,senddefineRequestType遵循前面提到的惯例,其中传递给回调的第一个参数是故障原因,如果有的话,第二个参数是实际结果。

这些可以由我们的包装翻译成Promise的解析和拒绝。

  1. class Timeout extends Error {}
  2. function request(nest, target, type, content) {
  3. return new Promise((resolve, reject) => {
  4. let done = false;
  5. function attempt(n) {
  6. nest.send(target, type, content, (failed, value) => {
  7. done = true;
  8. if (failed) reject(failed);
  9. else resolve(value);
  10. });
  11. setTimeout(() => {
  12. if (done) return;
  13. else if (n < 3) attempt(n + 1);
  14. else reject(new Timeout("Timed out"));
  15. }, 250);
  16. }
  17. attempt(1);
  18. });
  19. }

因为Promise只能解析(或拒绝)一次,所以这个是有效的。 第一次调用resolvereject会决定Promise的结果,并且任何进一步的调用(例如请求结束后到达的超时,或在另一个请求结束后返回的请求)都将被忽略。

为了构建异步循环,对于重试,我们需要使用递归函数 - 常规循环不允许我们停止并等待异步操作。 attempt函数尝试发送请求一次。 它还设置了超时,如果 250 毫秒后没有响应返回,则开始下一次尝试,或者如果这是第四次尝试,则以Timeout实例为理由拒绝该Promise

每四分之一秒重试一次,一秒钟后没有响应就放弃,这绝对是任意的。 甚至有可能,如果请求确实过来了,但处理器花费了更长时间,请求将被多次传递。 我们会编写我们的处理器,并记住这个问题 - 重复的消息应该是无害的。

总的来说,我们现在不会建立一个世界级的,强大的网络。 但没关系 - 在计算方面,乌鸦没有很高的预期。

为了完全隔离我们自己的回调,我们将继续,并为defineRequestType定义一个包装器,它允许处理器返回一个Promise或明确的值,并且连接到我们的回调。

  1. function requestType(name, handler) {
  2. defineRequestType(name, (nest, content, source,
  3. callback) => {
  4. try {
  5. Promise.resolve(handler(nest, content, source))
  6. .then(response => callback(null, response),
  7. failure => callback(failure));
  8. } catch (exception) {
  9. callback(exception);
  10. }
  11. });
  12. }

如果处理器返回的值还不是PromisePromise.resolve用于将转换为Promise

请注意,处理器的调用必须包装在try块中,以确保直接引发的任何异常都会被提供给回调函数。 这很好地说明了使用原始回调正确处理错误的难度 - 很容易忘记正确处理类似的异常,如果不这样做,故障将无法报告给正确的回调。Promise使其大部分是自动的,因此不易出错。