Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SDK

Dr Strange 提供六种语言的客户端库:TypeScript、Python、Go、Java、C 与 Zig。 每一种都通过 JSON-RPC 2.0 与正在运行的 drsg serve 通信,且其带类型的方法接口是 由服务端的 OpenRPC schema 生成的,因此每个 SDK 都与线协议精确一致,并在各版本间 与之保持同步。(Zig 客户端是对生成的 C 客户端的一层胶水封装,因此继承同样的保证。)

获取 SDK

各 SDK 位于仓库的 sdk/<语言> 目录下,版本号与工作区一致,均为 2.27.1。目前没有任何 一个 SDK 发布到语言包仓库(没有 PyPI、npm、Maven Central 发行版,也没有 sdk/go/vX 标签),因此都需从本仓库的检出安装:

语言位置从仓库安装
TypeScriptsdk/typescript在该目录 bun install && bun run build,然后在 package.json 中写 "drsg": "file:…/sdk/typescript"
Pythonsdk/pythonpip install …/sdk/python(或 pip install "drsg @ git+https://github.com/wangyingsm/dr-strange.git#subdirectory=sdk/python")
Gosdk/gogo get github.com/wangyingsm/dr-strange/sdk/go@<commit>,或用 replace … => …/sdk/go 指令
Javasdk/java在该目录 ./mvnw install,然后依赖 io.github.wangyingsm:drsg:2.27.1
Csdk/cmake → libdrsg.a + include/drsg.h(需要 libcurl + json-c)
Zigsdk/zigzig build,或将 src/drsg.zig 作为模块引入(Zig 0.16)

各目录的 README.md 给出了该语言的具体步骤。

连接与调用

客户端由一个基地址与一个令牌构造;令牌默认取自 DRSG_TOKEN 环境变量,并以 Authorization: Bearer 凭据的形式随每个请求(包括变更流背后的 WebSocket 升级请求) 一同发送,绝不放在 URL 中。(只有在标准 WebSocket 构造器无法设置请求头的运行时—— 浏览器窗口或 Worker,以及 Deno——TypeScript 客户端才会在套接字上回退到 ?token=; Bun 与 Node 发送请求头。)方法名与 RPC 方法一一对应, 并适配各语言的命名习惯:

语言构造客户端调用示例
TypeScriptnew Drsg({ baseUrl, token })await db.nodeCreate({ … })
PythonDrsg(base_url=…, token=…)db.node_create(…)
Godrsg.New(drsg.WithBaseURL(…), drsg.WithToken(…))db.NodeCreate(ctx, …)
Javanew Drsg(baseUrl, token)db.nodeCreate(…)
Cdrsg_client_new(base_url, token)drsg_node_create(…)
Zigtry drsg.Client.init(base_url, token)c.drsg_node_create(client.handle, …)

其形态是统一的。以 TypeScript 为例:

import { Drsg } from "drsg";

const db = new Drsg({ baseUrl: "http://127.0.0.1:7700", token: process.env.DRSG_TOKEN });

await db.nodeCreate({ plane: "social", key: "ada", labels: ["Person"] });
await db.nodeCreate({ plane: "social", key: "alan", labels: ["Person"] });
await db.edgeCreate({ plane: "social", src: "ada", dst: "alan", type: "KNOWS" });

const stats = await db.dbStats();
console.log(stats.nodes, stats.edges);

其它语言遵循同一套方法接口,只是各自用本地习惯的写法:Go 在每次调用中传入一个 context.Context;Python 与 Java 直接抛出异常;C 返回一个由调用方持有的 json_object,并通过一个出参报告失败。

错误处理

应用级失败(未知平面、非法计划)是一个 JSON-RPC 错误;被拒绝的凭据对应错误码 -32001。各 SDK 都会把它包装成带类型的错误:TypeScript 与 Python 中的 DrsgError / DrsgAuthError(此外 TypeScript 客户端中超过 timeoutMs 的请求抛出 DrsgTimeoutError,Python 中 非 JSON-RPC 的响应或畸形的变更流帧抛出 DrsgProtocolError),Go 中带 IsAuthError 的 *drsg.Error,Java 中的 DrsgException / DrsgAuthException,以及 C 中一个填充好的 drsg_error(配 drsg_is_auth_error)。

变更流

除 Zig 外,每个 SDK 都能打开一条长连接 WebSocket,订阅某个平面的变更流(第 3 章) (Zig 绑定只封装 C 客户端的请求/响应接口;需要变更流的 Zig 程序可直接调用 C 库的 drsg_watch), 接收每一个已提交的 ChangeEvent:结构为 { plane, seq, truncated, changes },其中每个 变更为 { kind, op, id, labels?, record? }。订阅遵循各语言自然的并发模型:

TypeScript —— 一个回调;套接字自动重连。close() 停止它。

const sub = db.watch("social", (event) => {
  for (const c of event.changes) console.log(event.seq, c.op, c.kind, c.id);
});
// sub.close();

Python —— 一个阻塞式生成器;迭代以消费,跳出以断开。

for event in db.watch("social"):
    for c in event["changes"]:
        print(event["seq"], c["op"], c["kind"], c["id"])

Go —— 一个通道(channel);取消 context 以停止。拨号与握手同样遵守 context(无截止时间则为 30 秒),服务端断开时通道关闭,超过 drsg.MaxFrameBytes 的消息会结束订阅。

events, _ := db.Watch(ctx, "social")
for e := range events {
    for _, c := range e.Changes {
        fmt.Println(e.Seq, c.Op, c.Kind, c.ID)
    }
}

Java —— 一个监听器;返回的 Subscription 关闭它(先发送关闭帧,再中止套接字,沉默的对端无法让它悬着)。监听器抛出的异常以 WARNING 记录日志,订阅继续。客户端本身是 AutoCloseable 的:close() 结束其订阅并回收自建的 HttpClient(通过 Client.Options 共享的除外)。

var sub = db.watch("social", null, event -> {
    for (var c : event.changes()) System.out.println(event.seq() + " " + c.op() + " " + c.kind());
});
// sub.close();

C —— 一个回调;drsg_watch 阻塞,直到回调返回非零值(如有需要,请在一个线程上 运行它;drsg_watch_cancellable 接受一个 drsg_watch_ctl,其他线程可通过 drsg_watch_ctl_cancel 取消)。一个 drsg_client 只包装一个 libcurl 句柄,不能被两个 线程同时使用,因此 watch 线程需要自己的客户端。超过 DRSG_WS_MAX_MESSAGE_BYTES 的消息会以 DRSG_TRANSPORT_ERROR_CODE 结束该 watch。

static int on_change(struct json_object *event, void *userdata) {
    /* 检视 event["changes"];返回非零值以停止 */
    return 0;
}
drsg_error err;
drsg_watch(client, "social", NULL, on_change, NULL, &err);

一个可选的标签会将订阅收窄到对该标签节点的变更。投递是尽力而为的:落后过多的订阅者 会丢弃溢出部分,而不会拖住写入方。

由于每个事件都携带其落库时的提交序号,订阅者可以分别读取该序号和前一个序号处的 as_of 状态,从而重建一次变更前后的确切状态(第 4 章)。

代码生成

每个 SDK 带类型的方法接口都由 crates/dr-strange-web/openrpc.json 生成,这也是服务端 从 rpc.discover 返回的唯一权威来源。每个 SDK 都携带一个小型代码生成器与一个漂移 (drift)测试;一旦已提交的客户端不再与该 schema 匹配,测试便会失败,因此这些库无法 在无声无息中偏离线协议。生成的接口之下是手写的部分:传输层、错误类型,以及 WebSocket 的 watch。