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
标签),因此都需从本仓库的检出安装:
| 语言 | 位置 | 从仓库安装 |
|---|---|---|
| TypeScript | sdk/typescript | 在该目录 bun install && bun run build,然后在 package.json 中写 "drsg": "file:…/sdk/typescript" |
| Python | sdk/python | pip install …/sdk/python(或 pip install "drsg @ git+https://github.com/wangyingsm/dr-strange.git#subdirectory=sdk/python") |
| Go | sdk/go | go get github.com/wangyingsm/dr-strange/sdk/go@<commit>,或用 replace … => …/sdk/go 指令 |
| Java | sdk/java | 在该目录 ./mvnw install,然后依赖 io.github.wangyingsm:drsg:2.27.1 |
| C | sdk/c | make → libdrsg.a + include/drsg.h(需要 libcurl + json-c) |
| Zig | sdk/zig | zig build,或将 src/drsg.zig 作为模块引入(Zig 0.16) |
各目录的 README.md 给出了该语言的具体步骤。
连接与调用
客户端由一个基地址与一个令牌构造;令牌默认取自 DRSG_TOKEN 环境变量,并以
Authorization: Bearer 凭据的形式随每个请求(包括变更流背后的 WebSocket 升级请求)
一同发送,绝不放在 URL 中。(只有在标准 WebSocket 构造器无法设置请求头的运行时——
浏览器窗口或 Worker,以及 Deno——TypeScript 客户端才会在套接字上回退到 ?token=;
Bun 与 Node 发送请求头。)方法名与 RPC 方法一一对应,
并适配各语言的命名习惯:
| 语言 | 构造客户端 | 调用示例 |
|---|---|---|
| TypeScript | new Drsg({ baseUrl, token }) | await db.nodeCreate({ … }) |
| Python | Drsg(base_url=…, token=…) | db.node_create(…) |
| Go | drsg.New(drsg.WithBaseURL(…), drsg.WithToken(…)) | db.NodeCreate(ctx, …) |
| Java | new Drsg(baseUrl, token) | db.nodeCreate(…) |
| C | drsg_client_new(base_url, token) | drsg_node_create(…) |
| Zig | try 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。