pub fn archive(self : State) -> Unit {self.archived = truelet name = @archive.DocumentName("TODO")
@archive.archive.insert(name, self.document.iter().to_array())
}
请注意,到目前为止,我们在
State 中没有存储文档的名称——我们并没有把它存储在任何地方。这是故意的,正如我们之前讨论的那样,工人的名称将作为文档的唯一标识符。一旦我们进入 Golem 特定的实现阶段,获取工人的名称将以 Golem 特有的方式完成。
发送一封电子邮件
我们已经在
State 类型中准备好了部分电子邮件发送逻辑:它有一个截止日期和一份收件人名单。我们的想法是,当创建一个新的列表时,我们会启动一个电子邮件发送工人,并让它与我们的编辑会话并行运行,形成一个循环。在这个循环中,首先查询列表编辑状态中的截止日期和收件人名单,然后它会一直休眠直到指定的截止日期。当它醒来时(12 小时后),它再次查询列表,如果已经过了截止日期,说明在此期间没有进一步的编辑操作。然后,它会向收件人列表发送通知邮件。
有了这些,我们就可以调用 WASI 的
now 函数来查询当前的系统时间,并将其转换为我们之前使用的
datetime 模块的
DateTime 类型:
///| Queries the WASI wall clock and returns it as a @datetime.DateTime////// Note that DateTime has only millisecond precisionfn now() -> @datetime.DateTime {let wasi_now = @wallClock.now()let base_ms = wasi_now.seconds.reinterpret_as_int64() * 1000;let nano_ms = (wasi_now.nanoseconds.reinterpret_as_int() / 1000000).to_int64();
@datetime.DateTime::from_unix_mseconds(base_ms + nano_ms)
}
使用 MoonBit 在Golem Cloud上开发应用程序
介绍
Golem 架构
初步 MoonBit 实现
moon new创建一个新的 lib 项目。这将创建一个包含单个包的新项目。为了匹配我们的架构,我们将开始创建多个包,每个包对应一个要开发的组件(列表、归档、邮件)。moon.pkg.json文件:列表模型
Document实现方法,支持我们希望的文档编辑操作。在此级别,我们不关心协作编辑或连接的用户,只需将文档建模为纯数据结构:test "new document is empty" {let empty = Document::new()assert_eq!(empty.items, []) }inspect函数,测试可以使用快照值来进行比较。moon CLI工具和 IDE 集成提供了在需要时自动更新这些测试函数中的快照值(content=部分)的方法:列表编辑器状态
Document类型的编辑器状态管理。提醒一下,我们决定每个列表组件的实例(Golem 工作者)只负责编辑一个单一的列表。因此,我们不需要关心存储和索引列表,或将连接路由到对应的节点,Golem 会自动管理这一切。Document类型,因此让我们继续定义State字段中使用的其他类型。ConnectionId将是一个包装整数的新类型:Map的键,因此我们需要为它实现Eq和Hash类型类。MoonBit 可以自动为新类型派生这些类型类。除此之外,我们还定义了一个名为next的方法,用于生成一个递增的连接 ID。EditorState结构存储每个连接的编辑者的信息。为了简化起见,我们只存储编辑者的电子邮件地址和自上次轮询以来的更改事件缓冲区。String类型:Change枚举描述了对文档所做的可能更改:///| 编辑文档时的可观察更改enum Change {Added(String)Deleted(String)Inserted(after~ : String, value~ : String) } derive(Show)Show(或手动实现),可以使用inspect测试函数比较更改数组的字符串快照与poll函数的结果。EditorState:///| 每个连接的编辑者的状态struct EditorState { email : EmailAddressmut events : Array[Change] }email字段在连接的编辑者中始终不变,但events数组会随着每次调用poll被重置,以便下次轮询时仅返回新的更改。为了实现这一点,我们必须将其标记为可变(mut)。State引入的最后一个新类型是表示时间点的东西。MoonBit 的核心标准库目前没有这个功能,但已经有一个名为mooncakes的包数据库,里面发布了 MoonBit 的包。在这里我们可以找到一个叫datetime的包。通过使用 moon CLI 可以将其添加到项目中:moon.pkg.json将其导入:{"import": ["suiyunonghen/datetime"]}@datetime.DateTime引用该包中的DateTime类型。State的方法之前,我们还需要考虑错误处理——State上的某些操作可能会失败,例如如果使用了错误的连接 ID,或者如果文档已归档时仍进行编辑操作。MoonBit 内建了错误处理支持,首先通过以下方式定义我们自己的错误类型:State的所有方法,但完整的源代码可以在 GitHub 上找到。connect方法将新的连接 ID 与连接的用户关联,并返回当前文档状态。这对于使用poll的结果非常重要——返回的更改列表必须精确应用到客户端上的该文档状态。Document定义的编辑操作的基础上,但除了这些,它们还执行以下任务:Change事件email_deadline和email_recipients字段,因为每次编辑操作都会重置发送电子邮件的超时时间Unit!EditorError返回类型表示这些方法可能会失败,并返回EditorError。datetime库没有获取当前日期和时间的功能,我们需要这个功能来使此函数正常工作。我们将在针对 WebAssembly(和 Golem)时解决这个问题,因为获取当前系统时间依赖于目标平台。add,是直截了当的:poll也很简单,因为我们已经为每个连接维护了更改列表,我们只需要在每次调用后重置它:列表归档
Document类型,因为它表示的是一个可编辑的文档。相反,我们在归档包中定义了一些新类型:insert方法和一个迭代所有已归档文档的方法:Archive实例来模拟这一点:State::archive方法中调用它:State中没有存储文档的名称——我们并没有把它存储在任何地方。这是故意的,正如我们之前讨论的那样,工人的名称将作为文档的唯一标识符。一旦我们进入 Golem 特定的实现阶段,获取工人的名称将以 Golem 特有的方式完成。发送一封电子邮件
State类型中准备好了部分电子邮件发送逻辑:它有一个截止日期和一份收件人名单。我们的想法是,当创建一个新的列表时,我们会启动一个电子邮件发送工人,并让它与我们的编辑会话并行运行,形成一个循环。在这个循环中,首先查询列表编辑状态中的截止日期和收件人名单,然后它会一直休眠直到指定的截止日期。当它醒来时(12 小时后),它再次查询列表,如果已经过了截止日期,说明在此期间没有进一步的编辑操作。然后,它会向收件人列表发送通知邮件。编译为 Golem 组件
绑定
wit-bindgen 工具已经支持 MoonBit,因此我们可以首先安装最新版本:wit-bindgen,但该版本还不支持 MoonBit。新版本应该可以很好地工作,但 Golem 的示例代码并没有在这个版本上进行过测试。State类型实现的方法。另一个是一个内部 API,用于电子邮件组件查询截止日期和收件人,如前所述。uri。这是必要的,因为电子邮件工人需要调用它所从中生成的特定列表工人。具体细节稍后会解释。wit目录中,依赖项放在wit/deps的子目录中。可以参考 仓库中的示例。moon.mod.json标识),并仅将list、email和archive创建为内部包。此时,我们需要做一些更改,因为我们需要为每个我们想要编译成独立 Golem 组件的代码块创建一个单独的模块。通过在每个子目录中运行wit-bindgen(如下所示),它实际上会为我们生成模块定义。src/archive移到archive等,并将之前编写的源代码移到archive/src。这样生成的绑定和我们手写的实现将并排放置。我们还可以删除顶级模块定义的 JSON 文件。stub.wit文件,再次运行此命令将会覆盖我们的更改。为避免这种情况,可以使用以下方式运行:./target/wasm/release/build/gen/gen.wasm的 WASM 模块。这还不是一个 WASM 组件——因此不能直接在 Golem 中使用。为了实现这一点,我们需要使用另一个命令行工具wasm-tools,将该模块转换为一个自描述其高级导出接口的组件。WIT 依赖项
wit-deps工具。wit-deps:wit目录中创建一个deps.toml文件,内容如下:wit/deps目录:实现导出功能
archive/gen/interface/demo/archive/api/stub.mbt位置生成了一个stub.mbt文件,其中包含两个需要实现的导出函数。我们在使用代码生成器时通常会遇到一个问题:我们在 WIT 中定义了archived-list,并且绑定生成器根据它生成了以下 MoonBit 定义:ArchivedDocument!唯一的区别是使用了DocumentName新类型,并且我们的版本派生了一个Show实例。我们可以决定放弃使用这个新类型,并在我们的业务逻辑中使用生成的类型,或者我们可以保持生成的类型与我们的实际代码分离。(这其实并不特定于 MoonBit 或 WASM 工具链,在任何基于代码生成器的方法中都会遇到这个问题。)stub.mbt文件。store。我们可以通过调用insert在我们的单例顶级Archive中实现它,就像我们之前直接将归档包连接到列表包时做的那样:pub fn store(name : String, items : Array[String]) -> Unit { @src.archive.insert(@src.DocumentName(name), items) }stub包的 JSON 中导入我们的主归档源:{"import": [{ "path" : "demo/archive/ffi", "alias" : "ffi" },{ "path" : "demo/archive/src", "alias" : "src" }]}ArchivedDocument结构体设置为pub,否则我们无法从stub包访问它的name和items字段。list/gen/interface/demo/lst/api/stub.mbt和list/gen/interface/demo/lst/emailQuery/stub.mbt中),使用我们现有的State实现。EditorError的失败映射到 WIT 定义中使用的字符串错误。首先我们为EditorError定义一个to_string方法:pub fn to_string(self : EditorError) -> String {match self {InvalidConnection(id) => "Invalid connection ID: \{id._}", AlreadyArchived => "Document is already archived" } }?和map_err:pub fn add(c : Connection, value : String) -> Result[Unit, String] { @src.state .add?(to_connection_id(c), value) .map_err(fn(err) { err.to_string() }) }使用宿主函数
update_email_properties函数时,我们无法正确查询当前时间来计算适当的截止日期。现在我们正在面向 Golem,我们可以使用 WebAssembly 系统接口(WASI)来访问诸如系统时间之类的功能。一种方法是使用已发布的wasi-bindings 包,但既然我们已经从 WIT 生成绑定,我们可以直接使用我们自己生成的绑定来导入宿主函数。world lst { export api; export email-query; import wasi:clocks/wall-clock@0.2.0; }--ignore-stub,以避免覆盖我们的存根实现!),并将其导入到我们的主包(src)中:{"import": ["suiyunonghen/datetime",{ "path" : "demo/lst/interface/wasi/clocks/wallClock", "alias" : "wallClock" }]}now函数来查询当前的系统时间,并将其转换为我们之前使用的datetime模块的DateTime类型:Golem 应用清单
golem-cli生成必要的文件来进行工作者之间的通信,并且也更容易将编译后的组件部署到 Golem。构建步骤
wit-bindgen ... --ignore-stub重新生成 WIT 绑定。moon build --target wasm将 MoonBit 源代码编译为 WASM 模块。wasm-tools component embed将 WIT 规范嵌入到自定义的 WASM 部分中。wasm-tools component new将 WASM 模块转换为 WASM 组件。清单模板
golem.yaml。接下来,我们设置一个临时目录和一个共享目录,用于存放我们之前通过wit-deps获取的 WIT 依赖:deps.toml移动到common-wit目录,并在根目录执行wit-deps update,我们可以将所有需要的 WASI 和 Golem API 填充到这个依赖目录中。target/release目录下。golem app clean命令中被清理,还可以定义自定义命令供golem app xxx执行:golem.yaml来将新的 MoonBit 模块添加到这个 Golem 项目中——比如archive/golem.yaml和list/golem.yaml。构建组件
golem app build会对 WIT 定义进行一些转换。这意味着我们之前编写的存根文件位置已经不正确。修复这个问题最简单的方法是删除所有由wit-bindgen生成的目录(但首先要备份手写的存根文件!),然后将存根文件复制回新创建的目录中。我们在这里不会进一步讨论这个问题。本文的博客会逐步介绍如何使用 MoonBit 构建 Golem 应用,并且在后期介绍应用清单,但推荐的方法是从一开始就使用应用清单,这样就无需做这些修复了。初次尝试
store函数,然后调用get-all函数,使用 CLI 的worker invoke-and-await命令来尝试归档组件:store函数,然后调用get-all函数,使用 CLI 的worker invoke-and-await命令来尝试归档组件:--build-profile debug,我们还会看到一个漂亮的调用栈):State,并且在它的构造函数中尝试调用一个 WASI 函数(获取当前的日期和时间)。这个时机太早了;所以我们需要修改State::new方法,避免在初始化时调用任何宿主函数:Worker 到 Worker 通信
列表调用归档
archive()时,它需要在一个单例的归档 worker 中调用store函数,并将数据发送过去。golem app build会进行许多新的构建步骤——包括生成和编译一些 Rust 源代码,这些代码在 Golem 的下一个版本中将不再需要。store函数。GOLEM_WORKER_NAME环境变量设置为 worker 的名称,我们也可以通过自定义环境变量手动提供值给 worker。这使得我们可以从外部注入组件 ID(直到 Golem 1.2 版本添加了更复杂的配置功能)。golem app build以重新生成绑定,并在list/src的 MoonBit 包中导入它:{ "path" : "demo/lst/interface/wasi/cli/environment", "alias": "environment" }ARCHIVE_COMPONENT_ID:get-all——我们可以看到远程过程调用是有效的!列表与电子邮件组件通信
wasm-rpc),同时电子邮件也依赖于列表(同样通过wasm-rpc)。我们需要在两个方向上进行通信。subscribe-instant函数。send-email函数的 MoonBit 实现(我们已在email.wit文件中定义该函数):wallClock接口来查询当前时间,并根据从相关列表 worker 获取的截止日期计算等待的时长。get_deadline和get_recipients方法则是利用 Golem 的 worker-to-worker 通信进行的。1. 休眠功能
subscribe-duration函数来获取一个可轮询对象,并在该对象上进行轮询,从而实现休眠。由于我们只传递了一个单一的轮询对象给列表,它将在目标截止日期到达时返回:2. 列表中的非阻塞调用
发送电子邮件
https://api.sendgrid.com/v3/mail/send发送一个 HTTP POST 请求,并附上已配置的授权头和描述电子邮件发送请求的 JSON 正文。Result类型的返回值,因此我们的代码将会比较冗长:request变量,包含了发送 HTTP 请求所需的所有内容,接下来我们可以调用handle函数来发起 HTTP 请求:调试
golem app build --build-profile debug),Golem 会在 MoonBit 组件出现问题时显示详细的堆栈追踪。另一种观察 worker 的有用方法是,在其中写入日志,可以通过golem worker connect或 Golem 控制台实时查看(或稍后查询)。结论
wit-bindgen moonbit生成的目录结构刚开始时确实让人感到有些压倒性。