For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/1/proposal-eventual-send.md.
  • 简体中文
  • Support for Distributed Promise Pipelining S1

    中文标题:支持分布式 Promise 流水线

    提案概览
    提案速览

    该提案向 JavaScript Promise 添加了最终发送操作,以支持分布式 promise 流水线,从而可以调用可能远程的对象上的操作。它引入了带有处理程序陷阱(eventualGet、eventualApply、eventualSend)的委托 Promise,以及 Promise.delegated 上的静态方法,遵循代理类比。该提案还包括用于远程方法调用的 E(target) 便捷代理制造者。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Promise.delegated

    为分布式 promise 流水线提供 API 支持。

    • Mark S. Miller @erights, Agoric
    • Michael FIG @michaelfig, Agoric
    • Chip Morningstar @FUDCo, Evernote

    状态

    已提交至 TC39(JavaScript 标准委员会),并达到 stage 1。(请注意,自该演讲以来,实际 API 已修改为使用 Promise.delegated 和其他 Promise 静态方法,而不是新的 HandledPromise 全局对象。)

    向 TC39 做报告

    幻灯片

    背景

    Promise 发明于 1980 年代末,最初是作为一种补偿通过网络远程调用操作时往返延迟的技术,尽管此后 Promise 已被证明对处理计算系统中各种异步延迟都有价值。

    Promise 背后的基本洞见是这样的:在面向对象编程的经典表述中,对象是你可以向其发送消息以调用其操作的东西。如果此类操作的结果是另一个对象,那么该结果反过来又是你可以向其发送消息的东西。如果由消息发起的操作涉及获取结果的异步延迟,与其强迫发送者等待(可能等待很长时间)直到结果最终可用,系统可以改为立即返回另一个对象——一个 Promise——在等待期间代表结果。由于正如刚才所说,对象是你可以发送消息的对象,因此从这个意义上说,Promise 可能与其所承诺的对象一样好——你只需像对待实际结果一样向它发送消息。

    Promise 无法直接执行所调用的操作,因为其含义尚不清楚,但它可以将请求排队以供稍后处理,或将其转发到网络连接的另一端,最终结果将在那里得知。这种通过排队或转发来延迟操作的方式可以流水线化任意深度的操作;只有在确实需要查看结果的语义要求(例如需要将其显示给人)时,流水线才必须停止以等待最终结果。此外,此范式的经验表明,真正需要这种等待的时机,往往比许多人凭直觉预期的计算活动链中的点要晚得多。

    由于网络延迟通常是远程调用中延迟的最大组成部分,因此 promise 流水线所实现的重叠网络传输可以极大地提高分布式系统的整体吞吐量。例如,在 Xanadu 超级文本系统 和微软的 Midori 操作系统 中,为远程方法调用实现的 promise 流水线,与传统同步 RPC 相比,根据用例不同,测得的加速比为 10 到 1000 倍。

    JavaScript 中的 Promise 是在 2011 年的 ECMAScript strawman 并发提案 中提出的。这些 Promise 源自 E 语言,通过 Waterken Q 库Kris Kowal 的 Q 库。早期一篇很好的介绍是 Tom Van Cutsem 的 在 JavaScript 中传达事件循环:一次探索。所有这些努力都将 Promise 作为迈向分布式计算的第一步,目标是将 Promise 用作对远程对象的异步引用。然而,由于 JavaScript 语言本身不包含任何固有的 I/O 机制,完全依赖宿主环境来实现这一点,因此 JavaScript 目前定义的 Promise 本身不足以实现最初激励它们的分布式计算愿景。

    Kris Kowal 的 Q-connection 库 通过 promise 流水线 扩展了 Q 的 Promise 以用于分布式计算,本质上符合我们的想法。然而,在没有 Weak References 的平台支持的情况下,这种方法并不实用。有了弱引用后,Midori 项目Cap'n Proto 等项目证明了这种分布式计算方法可以大规模良好运行。

    摘要

    本提案向 JavaScript Promise 添加了 最终发送 操作,以表达对可能远程的对象的操作调用。我们引入了 委托 Promise 的概念,它可以有一个处理程序来提供可选的最终发送行为。我们还引入了 Presence 的概念,它也可以有处理程序,但不是 Promise。这些机制与弱引用一起,使得创建远程对象通信系统成为可能,但无需承诺任何具体实现。特别是,本提案规定了一种通用机制,用于挂钩任何可用的宿主提供的远程通信设施,而不限制这些设施的性质。

    本提案不强制要求对其描述的机制进行任何特定使用。这里提到的此类用途仅作为解释性和激励性示例,以及测试设计充分性的方法,而不是提出远程消息传递的具体实现。

    设计原则

    1. 支持 promise 流水线 以降低网络延迟成本。
    2. 防止重入攻击(一种计划干扰形式)。

    详情

    为了指定最终发送操作和委托 Promise,我们遵循用于将代理合并到 JavaScript 中的模式:该模式指定了...

    • 内部方法 所有对象必须支持。
    • Reflect 上的 静态方法 用于调用这些内部方法。
    • 这些方法必须维护的 不变量
    • 这些方法对普通(非特殊)对象的 默认行为
    • 处理程序陷阱。代理通过将其大部分行为委托给其处理程序上相应的陷阱来实现这些方法。
    • 代理不变量执行。代理方法中的其余行为用于保证这些不变量尽管处理程序行为任意也能得到维护。
    • 对缺失陷阱的 回退行为,根据其余陷阱实现。

    遵循这个类比,本提案向所有 Promise 添加了内部最终发送方法,为未委托的 Promise 提供默认行为,并引入了委托 Promise,其处理程序为这些方法提供陷阱。

    一个新的静态方法 Promise.delegated 使得创建委托 Promise 成为可能。下面的静态方法是此制造者的静态方法,即 Promise.delegated.eventualGet 等:

    内部方法静态方法
    p.[[EventualGet]](prop, opts)eventualGet(p, prop, opts = {})
    p.[[EventualApply]](args, opts)eventualApply(p, args, opts = {})
    p.[[EventualSend]](prop, args, opts)eventualSend(p, prop, args, opts = {})

    静态方法首先对其第一个参数执行 Promise.resolve,将其强制转换为具有这些内部方法的 Promise。因此,例如,

    Promise.delegated.eventualGet(p, prop)

    实际上相当于

    Promise.resolve(p).[[EventualGet]](prop, {})

    通过内部方法,静态方法会导致默认行为,或者对于委托 Promise,导致调用关联处理程序陷阱的行为。

    静态方法默认行为处理程序陷阱
    eventualGet(p, prop, opts)p.then(t => t[prop])h.eventualGet(t, prop, { opts })
    eventualApply(p, args, opts)p.then(t => t(...args))h.eventualApply(t, args, { opts })
    eventualSend(p, prop, args, opts)p.then(t => t[prop](...args))h.eventualSend(t, prop, args, { opts })

    为了防止重入,代理内部方法将处理程序陷阱的执行推迟到后续轮次,并立即返回一个 Promise 来表示陷阱将返回的结果。例如,委托 Promise 的 [[EventualGet]] 内部方法实际上就是

    p.then(t => h.eventualGet(t, prop, { opts }))

    E 便捷代理制造者

    可能是最常见的分布式编程情况,即调用远程方法(无论是否要求返回值),都可以通过无能力代理来实现。启用对等方之间通信所需的所有权限都可以在委托 Promise 基础设施中实现。

    E(target) 代理制造者包装一个目标(可能是远程的,也可能不是),并允许一次远程方法调用返回一个 Promise 作为结果。

    E(target).method(arg1, arg2...) // Promise<result>

    用法示例:

    import { E } from '@agoric/far';
    
    // 调用流水线 RPC。
    const fileP = E(
      E(target).openDirectory(dirName)
    ).openFile(fileName);
    // 在一次往返后处理读取结果。
    E(fileP).read().then(contents => {
      console.log('文件内容', contents);
      // 我们不使用此发送的结果。
      E(fileP, { _oneway: true }).append('发送后不管');
    });

    Promise.delegated 函数

    以类似于 Proxy 处理程序的方式,委托 Promise 与处理程序对象关联。

    new Proxy(target, handler) -> 新的代理
    
    new Promise((resolve, reject) => {
      ...
      resolve(resolution) -> void
      reject(reason) -> void
      ...
    }) -> 新的未委托 Promise
    
    
    Promise.delegated((resolve, reject, resolveWithPresence) => {
      ...
      resolve(resolution) -> void
      reject(reason) -> void
      resolveWithPresence(presenceHandler) -> 新的 presence
      ...
    }, unfulfilledHandler) -> 新的委托 Promise

    例如,

    const delegatedExecutor = async (resolve, reject, resolveWithPresence) => {
      // 做可能延迟才能完成的事。
      const { err, presenceHandler, other } = await determineResolution();
      if (presenceHandler) {
        // presence 是一个新创建的 Object.create(null),其处理程序
        // 是 presenceHandler。下面的 targetP 将解析为此 presence。
        const presence = resolveWithPresence(presenceHandler);
        presence.toString = () => '我的特殊 Presence';
      } else if (err) {
        // 用 err 拒绝 targetP。
        reject(err);
      } else {
        // 将 targetP 解析为 other,如果有处理程序则使用 other 的处理程序。
        resolve(other);
      }
    };
    
    // 使用初始处理程序创建一个委托 Promise。
    // pendingHandler 可以推测性地向远程主机发送流量。
    const targetP = new Promise.delegated(delegatedExecutor, pendingHandler);
    E(E(targetP).remoteMethod(someArg, someArg2)).callOnResult(...otherArgs);

    处理程序不会暴露给委托 Promise 的用户,因此它提供了非特权客户端(使用 E 代理制造者或静态 Promise.delegated 方法)与实现通信机制的特权系统之间的安全分离。

    Promise.prototype

    由于委托 Promise 仍然是 Promise,它们可以在任何可以使用 Promise 的地方使用。但是,通过 Promise.resolve 的附加语义,可以检测对象是否为 presence。

    处理程序陷阱

    处理程序对象可以提供处理程序陷阱(eventualGeteventualApplyeventualSend)。

    ({
      eventualGet        (target, prop, modifiers):        Promise<result>,
      eventualApply      (target, args, modifiers):        Promise<result>,
      eventualSend       (target, prop, args, modifiers):  Promise<result>,
    })

    如果处理程序省略了陷阱,则调用关联操作会返回一个 Promise 拒绝。该行为的唯一例外是处理程序未提供 eventualSend 优化陷阱。那么,其默认实现是

    Promise.delegated.eventualApply(Promise.delegated.eventualGet(p, prop), args, opts)

    这种展开要求远程方法的 Promise 被不必要地具体化。

    对于待处理的处理程序,陷阱的 target 参数是未解决的委托 Promise,以便处理程序可以在 Promise 解决之前获得控制权。对于 presence 处理程序,陷阱的 target 参数是由 resolveWithPresence 创建的 presence。

    Promise.delegated 静态方法

    本节中的方法用于实现更高级别的通信原语,例如 E 代理制造者。

    这些方法类似于 Reflect API,但异步调用委托 Promise 的处理程序,无论目标是否已解决。这对于在确切目的地已知(即在委托 Promise 解决之前)之前允许消息流水线化是必要的。

    Promise.delegated.eventualGet(target, prop, opts = {}); // Promise<result>
    Promise.delegated.eventualApply(target, [...args], opts = {}); // Promise<result>

    eventualSend 调用结合了属性查找和函数应用,以便将它们与 eventualGet(其值被单独检查)区分开来,并使处理程序能够将这两个操作捆绑为一条消息。

    Promise.delegated.eventualSend(target, prop, args, opts = {}); // Promise<result>

    选择加入/选择退出修饰符

    处理程序陷阱的最后一个参数称为 modifiers,其构造如下:

    • 可以安全忽略的修饰符属性(选择加入修饰符)必须以 下划线 (_) 开头。
    • 必须检查所有其他(必需的、非下划线的、选择退出的)修饰符属性,如果 Promise 的处理程序不识别它们,则必须导致 Promise 被拒绝。
    • 任何调用者提供的 opts(默认为空对象 {}),作为 modifiers.opts 提供。同样的约定适用;可以安全忽略的 opts 必须以 下划线开头。
    • modifiers 的其他直接属性是实现定义的。
    • Promise.delegated 实现应强制处理程序陷阱检查必需的修饰符。如果任何必需的 modifiersmodifiers.opts 属性未被读取,实现应拒绝结果 Promise。

    这些约定有助于区分可选的优化提示修饰符和必需的行为变更修饰符。例如,modifiers.opts._oneway 可以安全忽略,因为它对于正确性不是严格必需的,但 modifiers.opts.after 不能被忽略。

    平台支持

    目前描述的所有上述行为都将在 Eventual Send Shim 中实现。然而,我们指定了一个关键行为,它很容易由合规平台提供,但在当前平台 Promise 之上模拟不可行。没有它,许多本应流水线化的情况无法做到,从而破坏了所需的排序保证。考虑:

    let pResolve;
    const p = new Promise(r => pResolve = r);
    E(p).foo();
    let qResolve;
    const q = new Promise.delegated(r => qResolve = r, qPendingHandler);
    pResolve(q);

    p 解析为 q 之后,延迟的 foo 调用应被转发到 q 并触发 qqPendingHandler。虽然 shim 可以猴子补丁 Promise 构造函数以提供修改后的 resolve 函数来做到这一点,但有许多内部解析步骤会绕过它。shim 无法检测到未解决的委托 Promise p 已被其中之一解析为未解决的委托 Promise q。相反,foo 调用将一直闲置,直到一次往返满足 q,从而

    • 失去 promise 流水线化的好处,并且
    • 可能在其他消息之后到达,而实际上它应该在其他消息之前到达。

    语法支持

    一个单独的 Wavy Dot 提案 提出了更方便的语法来调用这里提出的新内部方法。然而,即使没有波浪点语法,这里描述的最终发送 API 也是有价值的。

    完成代理类比

    • 内部方法 所有 Promise 必须支持
      • [[EventualGet]],
      • [[EventualApply]],
      • [[EventualSend]]
    • Promise.delegated 上用于调用这些内部方法的 静态方法
      • Promise.delegated.eventualGet
      • Promise.delegated.eventualApply
      • Promise.delegated.eventualSend
    • 这些方法必须维护的 不变量
      • 重入安全性。
      • p === Promise.resolve(t) vs p.then(t => ...)
    • 这些方法对未委托 Promise 到普通对象的 默认行为
      • p~.foo ==> p.then(t => t.foo)
      • p~.(x) ==> p.then(t => t(x))
      • p~.foo(x) ==> p.then(t => t.foo(x))
    • 处理程序陷阱。代理通过将其大部分行为委托给其处理程序上相应的陷阱来实现这些方法。
      • p~.foo ==> p.then(t => h.eventualGet(t, 'foo'))
      • p~.(x) ==> p.then(t => h.eventualApply(t, [x])
      • p~.foo(x) ==> p.then(t => h.eventualSend(t, 'foo', [x])
    • Promise 不变量执行
      • 上面的 p.then 模式
    • 对缺失陷阱的 回退行为,根据其余陷阱实现。
      • h.eventualSend(t, 'foo', [x]) 默认回退到 h.eventualApply(t, h.eventualGet(t, 'foo'), [x])