🐟

SurrealDB の JavaScript SDK を作った

2024/09/08に公開

成果物

ソースコード:
https://github.com/tai-kun/surrealdb.js

ドキュメント:
https://tai-kun.github.io/surrealdb.js/ja/v2/getting-started/

はじめに

SurrealDB の存在は 2022 年に Qiita の記事で初めて知りました。

https://qiita.com/silane1001/items/795c3539675e588c2c4d

最強かどうかはさておき、個人的には文法と認証周りが気に入りました。ただ当時は v1 までしばらく掛かりそうな雰囲気だったので、安定したら試してみるかと思いつつ 1 年半以上も経ち、現在 (2024/9) では v2 のベータ版まで出ています。

通常「ブラウザー <-> サーバー <-> DB」でデータをやり取りしますが、「ブラウザー <-> DB」で直接やり取りできるように設計 (レコードアクセスという機能が実装) されているようです。そのため JavaScript SDK にお世話になるだろうと思い公式の JavaScript SDK を覗いてみましたが、自分の用途で使うにはやや機能不足でした。

https://github.com/surrealdb/surrealdb.js

機能をリクエストすることもできますが、案が煮詰まっていくと既存の実装をほぼ全て変えることになりそうだということが分かったので、公式とは異なるもう一つの SDK として公開した次第です。

公式 SDK との相違点

JSON サポート

SurrealDB とのやり取りでは CBOR というデータフォーマットを使うことができます。Superjson のようにエンコーダーとデコーダーを実装すれば SurrealDB が実装するデータ型 (Datetime, Uuid, Geometry, Decimal など) を見分けることができます。もちろん bigint も使えます。

SurrealDB 自体は JSON もサポートしていますが、公式の JavaScript SDK では JSON を使えず、CBOR しか使えません。あえて CBOR を使わずに JSON を使う場面があるので、この 2 つのデータフォーマットを切り替えられるようにしました。既存の実装をほぼ全て変える理由の半分くらいはこれです。

JSONフォーマッターの使用例
import { initSurreal } from "@tai-kun/surrealdb";
import JsonFormatter from "@tai-kun/surrealdb/formatters/json";

const { Surreal } = initSurreal({
  formatter: new JsonFormatter(),
  // ... その他の設定
});

const db = new Surreal();
await db.connect("ws://localhost:8000");

try {
  await db.signin({ user: "root", pass: "root" });
  await db.use("sample_namespace", "sample_database");
  const results = await db.query<[number]>(/*surql*/ `RETURN 42;`);
  console.log(results); // [ 42 ]
} finally {
  await db.disconnect();
}

ちなみに CBOR のエンコーダーとデコーダーを独自実装しており、公式と比較してエンコードは約 1.3 倍、デコードは約 2.7 倍高速です。主にバッファーのアロケート回数削減と文字列のエンコード/デコードをバッファーに書き込むアルゴリズムで速度改善しています。実装には RFC 8949 を参照しています (RFC 7049 の方ではありません)。

データ型の独自実装のサポート

データフォーマッターに CBOR を使うと SurrealDB が実装するデータ型のいくつかを JavaScript 側でも使えます。当 SDK では次のデータ型をサポートしています:

  • Datetime
  • Decimal
  • Duration
  • Table
  • Thing
  • Uuid
  • GeometryPoint
  • GeometryLine
  • GeometryPolygon
  • GeometryMultiPoint
  • GeometryMultiLine
  • GeometryMultiPolygon
  • GeometryCollection

また現在ベータ版の SurrealDB v2 に向けて次のデータ型もサポートする予定です:

  • Future
  • Range

SurrealDB が実装するデータ型のサポートまでは公式 SDK と同じですが、当 SDK では必要に応じて JavaScript 側の実装を変更できます。例えば Decimal は JavaScript 側で計算しないので、値を表現できさえすればそれで良いなら:

import { initSurreal } from "@tai-kun/surrealdb";
import {
  Datetime, Duration, Table, Thing, Uuid,
  GeometryCollection, GeometryLine, GeometryMultiLine, GeometryMultiPoint, GeometryMultiPolygon, GeometryPoint, GeometryPolygon,
} from "@tai-kun/surrealdb/data-types/standard";
import {
  Decimal,
} from "@tai-kun/surrealdb/data-types/encodable";
import CborFormatter from "@tai-kun/surrealdb/formatters/cbor";

export const { Surreal } = initSurreal({
  formatter: new CborFormatter({
    Uuid,
    Table,
    Thing,
    Decimal,
    Datetime,
    Duration,
    GeometryLine,
    GeometryPoint,
    GeometryPolygon,
    GeometryMultiLine,
    GeometryMultiPoint,
    GeometryCollection,
    GeometryMultiPolygon,
  }),
  // ... その他の設定
});

とすればバンドルサイズも削減できますし、逆にバンドルサイズを気にしないなら、当 SDK が提供するデータ型クラスを拡張して機能を追加することもできます。

インライン RPC/クエリー

これは実験的な機能です。インライン RPC/クエリー を使うとその場限りの RPC リクエストやクエリーを実行するのに毎度 Surreal クラスのインスタンスを構築する必要が無くなります:

インラインRPC/クエリーの例
import { query, rpc } from "@tai-kun/surrealdb";

const token = await rpc("http://127.0.0.1:8000", "signin", {
  params: [{
    user: "root",
    pass: "root",
  }],
});

const results = await query("http://127.0.0.1:8000", "RETURN 42", {}, {
  token,
  namespace: "test",
  database: "test",
});

自動再接続

これも実験的な機能です。WebSocket などのリアルタイム通信で発生する接続断に対し、自動的に再接続を試みる機能を提供します。一定のロジックに基づいて再接続を行い、成功・失敗のイベントを発行します。

ドキュメント: https://tai-kun.github.io/surrealdb.js/ja/v2/experimental/auto-reconnect/

その他

  • エラーハンドリングの改善
  • 異なる JavaScript ランタイム間の差異の吸収
  • HTTP と WebSocket の差異の吸収

公式 SDK は中止シグナル signal を使ったタイムアウトの実装が難しかったり (特に HTTP 接続時)、接続先 (URL、名前空間、データベース) の矛盾を検知しなかったり、致命的ではないエラー (単なるクエリーの失敗) 1 つで WebSocket の接続が切られたりなど、ラッパー無しでは実用が難しい仕様がいくつかありますが、API を考え直してなるべく Surreal クラス単体でも使いやすくしています。エラーに関してはドキュメントでガイドしています。

注意点

UNIX エポックより前の日時

SurrealDB の datetime 型はナノ秒精度まで保持できます。それゆえ JavaScript で扱うには注意が必要です。SurrealDB がシリアライズした datetime、例えば "1969-12-31T23:59:59.999999999Z" を JavaScript の Date.parse に渡すと WebKit では 0 ミリ秒になりますが、それ以外 (Node.js, Deno, Bun, Chromium, Firefox) は -1 ミリ秒になります。そして TC39 を見るとおそらく WebKit だけが正しいような... (要確認)。

CI

SDK とは直接関係ないのですが (品質には関係ある)、CI を頑張ってみました。次の JavaScript ランタイムでテストしています:

ランタイム バージョン
Node.js 18.x,20.x,^22.5.1
Deno ^1.x
Bun ^1.1.13
Chromium >=104
Firefox >=100
WebKit >=15.4

テストにはすべて Vitest を使用しています。ブラウザーでのテストは Vitest の Browser Mode を使っています。同様のテストランナーとして @web/test-runner がすでにあり、今までこれを使っていましたが、ビルドやモジュール解決の設定がやや苦しかったのと、サーバーとブラウザーのテストを同じコードで実行したい気持ちがあり、この度 Vitest に移ってみようかなと思いました。

参考までに、CI 上で Node.js と Browser Mode のテストの実行時間を比較してみると:

ランタイム 依存関係のインストール テスト
Node.js v22 7 秒 22 秒
Chromium 129 30 秒 31 秒

でした。テストは 1,300 弱あります。

さいごに

SurrealDB v2-beta は v1 と比べて使いやすくなっているので今後に期待大ですが、他の DB であっても使いやすさは Supabase や Prisma、Drizzle で改善されています。最強の DB かどうかは人それぞれの評価する箇所が違うので一概に言えませんが、自分が使う分には嬉しい機能が実装されていっているので、SDK もちまちま改善していくつもりです。GitHub 初心者で Issue テンプレートとかレビューとかよくわかりませんが、使ってみてバグあったら教えてください。

Discussion