mcpField
R
rivetkit-typescript

rivetkit-typescript

github.com/rivet-dev開発4460 回インストール1 日前に更新
リポジトリ

rivetkit-typescript について

rivetkit-typescriptは、Rivet ActorsとRivetKitで長時間稼働するステートフルなTypeScriptサービスを構築する開発者向けのスキルです。デプロイ、設計パターン、クライアント連携を扱い、リアルタイム協業やワークフロー、AIエージェントのバックエンドに役立ちます。

主な機能

  1. 01長時間稼働するステートフル計算
  2. 02永続化されるActor状態
  3. 03リアルタイムイベントとブロードキャスト
  4. 04Actorのシャーディングと調整パターン
  5. 05デプロイとトラブルシューティングの案内

活用シーン

  1. 01AIエージェントの会話メモリ構築
  2. 02リアルタイム共同作業アプリの実装
  3. 03バックグラウンドワークフローとキューの調整
  4. 04テナント別のデータ集約型バックエンド構成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rivet-dev/skills --skill rivetkit-typescript

インストール後、Claude Code で/rivetkit-typescriptを入力して読み込まれたか確認します。

詳細

Rivet Actors

Rivet のアクターランタイム上で、高性能で長期間稼働するインメモリプロセスを構築します。HTTP エンドポイント、データベース、キューでは、AI エージェント、リアルタイムコラボレーション、ワークフローのオーケストレーション、地理的に分散したデータの要件を満たせなくなった場合に、このスキルを使用してください。

行き詰まった場合の対処方法

意図したとおりに動作しない場合や行き詰まった場合は、ユーザーに Rivet Discord への参加、または問題を報告してサポートを受けるための Rivet GitHub への Issue の作成を案内してください。

以下の情報を含む、あらかじめ生成したレポートをユーザーに提供してください。

  • 症状
    • ローカル開発環境、デプロイ後、またはその両方で発生しているか
    • 表示されているエラー
    • 関連するソースコード
  • 解決のために試したこと
  • 環境
    • RivetKit のバージョン
    • ランタイム(Node、Bun など)とそのバージョン
    • 該当する場合、使用中のプロバイダー(Vercel、Railway など)
    • 該当する場合、使用中の HTTP ルーター(Hono、Express、Elysia など)

出典の引用

Rivet のドキュメントの情報を提供するときは、ユーザーが詳細を確認できるよう、正規 URL を引用してください。各リファレンスファイルのヘッダーメタデータには、正規 URL が含まれています。

引用方法:

  • 重要な概念にはインラインリンクを使用します:「アクターキーを使用して、インスタンスを一意に識別します。」
  • 複雑なトピックの説明の後には、「詳細はこちら」リンクを追加します

正規 URL の確認方法:

以下のリファレンスマップからリファレンスファイルにアクセスできます。各ファイルのヘッダーには次の内容が含まれています。

> Canonical URL: https://rivet.dev/docs/actors/actions

引用する際は、リファレンスファイルのパスではなく、その正規 URL を使用してください。

例:

  • Actions → https://rivet.dev/docs/actors/actions
  • React クライアント → https://rivet.dev/docs/clients/react
  • Kubernetes でのセルフホスト → https://rivet.dev/docs/self-hosting/kubernetes

最初の手順

  1. RivetKit をインストールします(最新:2.0.42-rc.1)
    npm install [email protected]
    
  2. setup({ use: { /* actors */ } }) でレジストリを定義します。
  3. registry.serve()、registry.handler()(サーバーレス)、または registry.startRunner()(ランナーモード)を公開します。ユーザーがランナーモードを使う具体的な理由がない限り、サーバーレスモードを推奨します。
  4. デプロイ前に、/api/rivet/metadata が 200 を返すことを確認します。
  5. Rivet Cloud またはセルフホストエンジンを設定します
  6. クライアントを統合します(JavaScript または React については、以下のクライアントガイドを参照してください)
  7. デプロイするかどうかをユーザーに尋ねます。デプロイする場合は、「Rivet バックエンドのデプロイ」に進みます。

詳細は、ユーザーのプロジェクトに適したクイックスタートガイドをお読みください。

Rivet バックエンドのデプロイ

特に指定がない限り、ユーザーは Rivet Cloud にデプロイするものとします。ユーザーがセルフホストする場合は、以下のセルフホストガイドをお読みください。

  1. ローカル開発環境で Rivet Actors が動作することを確認します
  2. デプロイ先のプロバイダーをユーザーに選んでもらいます(Vercel、Railway など、プロバイダーの一覧はConnectを参照してください)
  3. 選択したプロバイダーのデプロイガイドに従います。手動での操作が必要な場合は、その都度ユーザーに案内してください。

機能

  • 長期間稼働するステートフルなコンピューティング:各コンピューティング単位は、リクエスト間で情報を記憶する小さなサーバーのようなものです。データベースからデータを再取得したり、タイムアウトを心配したりする必要はありません。AWS Lambda に似ていますが、メモリがあり、タイムアウトもありません。
  • 超高速の読み取りと書き込み:状態はコンピューティングと同じマシンに保存されるため、読み書きは非常に高速です。データベースへの往復も、レイテンシーの急増もありません。状態は長期保存のために Rivet に永続化されるので、サーバーが再起動しても保持されます。
  • リアルタイム:WebSocket を使って状態を更新し、変更をリアルタイムにブロードキャストできます。外部の Pub/Sub システムもポーリングも不要で、低レイテンシーのイベントが組み込まれています。
  • 無制限のスケーラビリティ:アクターをゼロから数百万の同時実行数まで自動的にスケールできます。即時スケーリングとコールドスタートなしで、使用した分だけ支払います。
  • 耐障害性:エラー処理と復旧機能が組み込まれています。アクターは障害発生時に状態の整合性を維持し、処理を継続しながら自動的に再起動します。

Rivet Actors を使用する場面

  • AI エージェントとサンドボックス:複数ステップのツールチェーン、会話メモリ、サンドボックスのオーケストレーション。
  • マルチプレイヤーアプリやコラボレーションアプリ:CRDT ドキュメント、共有カーソル、リアルタイムダッシュボード、チャット。
  • ワークフローの自動化:バックグラウンドジョブ、cron、レートリミッター、永続キュー、バックプレッシャー制御。
  • データ集約型のバックエンド:地理的に分散したデータベースやテナントごとのデータベース、インメモリキャッシュ、シャーディングされた SQL。
  • ネットワーク処理:WebSocket サーバー、カスタムプロトコル、ローカルファースト同期、エッジへのファンアウト。

よく使われるパターン

アクターは、分離された状態とメッセージパッシングによって自然にスケールします。次のパターンを使ってアプリケーションを構成してください。

設計パターンのドキュメント

エンティティごとのアクター

ユーザー、ドキュメント、ルームごとにアクターを作成します。複合キーを使ってエンティティをスコープ指定します。

import { createClient } from "rivetkit/client";
import type { registry } from "./actors";

const client = createClient<typeof registry>();

// Single key: one actor per user
client.user.getOrCreate(["user-123"]);

// Compound key: document scoped to an organization
client.document.getOrCreate(["org-acme", "doc-456"]);
import { actor, setup } from "rivetkit";

export const user = actor({
  state: { name: "" },
  actions: {},
});

export const document = actor({
  state: { content: "" },
  actions: {},
});

export const registry = setup({ use: { user, document } });
コーディネーターとデータアクター

データアクターは中核となるロジック(チャットルーム、ゲームセッション、ユーザーデータ)を処理します。コーディネーターアクターはデータアクターのコレクションを追跡・管理します。インデックスのようなものと考えてください。

import { actor, setup } from "rivetkit";

// Coordinator: tracks chat rooms within an organization
export const chatRoomList = actor({
  state: { rooms: [] as string[] },
  actions: {
    addRoom: async (c, name: string) => {
      // Create the chat room actor
      const client = c.client<typeof registry>();
      await client.chatRoom.create([c.key[0], name]);
      c.state.rooms.push(name);
    },
    listRooms: (c) => c.state.rooms,
  },
});

// Data actor: handles a single chat room
export const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    send: (c, msg: string) => { c.state.messages.push(msg); },
  },
});

export const registry = setup({ use: { chatRoomList, chatRoom } });
import { createClient } from "rivetkit/client";
import type { registry } from "./actors";

const client = createClient<typeof registry>();

// Coordinator per org
const coordinator = client.chatRoomList.getOrCreate(["org-acme"]);
await coordinator.addRoom("general");
await coordinator.addRoom("random");

// Access chat rooms created by coordinator
client.chatRoom.get(["org-acme", "general"]);
シャーディング

負荷の高いアクターを、時間、ユーザー ID、ランダムキーで分割します。

import { createClient } from "rivetkit/client";
import type { registry } from "./actors";

const client = createClient<typeof registry>();

// Shard by hour
const hour = new Date().toISOString().slice(0, 13); // "2024-01-15T09"
client.analytics.getOrCreate(["org-acme", hour]);

// Shard randomly across 3 actors
client.rateLimiter.getOrCreate([`shard-${Math.floor(Math.random() * 3)}`]);
import { actor, setup } from "rivetkit";

export const analytics = actor({
  state: { events: [] as string[] },
  actions: {},
});

export const rateLimiter = actor({
  state: { requests: 0 },
  actions: {},
});

export const registry = setup({ use: { analytics, rateLimiter } });
ファンインとファンアウト

ワーカーに作業を分散(ファンアウト)し、結果を集約(ファンイン)します。

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

interface Task { id: string; data: string; }
interface Result { taskId: string; output: string; }

const coordinator = actor({
  state: { results: [] as Result[] },
  actions: {
    // Fan-out: distribute work in parallel
    startJob: async (c, tasks: Task[]) => {
      const client = c.client<typeof registry>();
      await Promise.all(
        tasks.map(t => client.worker.getOrCreate([t.id]).process(t))
      );
    },
    // Fan-in: collect results
    reportResult: (c, result: Result) => { c.state.results.push(result); },
  },
});

const worker = actor({
  state: {},
  actions: {
    process: async (c, task: Task) => {
      const result = { taskId: task.id, output: `Processed ${task.data}` };
      const client = c.client<typeof registry>();
      await client.coordinator.getOrCreate(["org-acme"]).reportResult(result);
    },
  },
});

const registry = setup({ use: { coordinator, worker } });
アンチパターン
「神」アクター

すべてを処理する単一のアクターは避けてください。ボトルネックを生み、アクターモデルの目的を損ないます。代わりに、エンティティごとに役割を絞ったアクターに分割してください。

リクエストごとのアクター

アクターはリクエストをまたいで状態を保持します。リクエストごとにアクターを作成すると、リソースを浪費し、永続状態のメリットも失われます。永続エンティティにはアクターを、ステートレスな処理には通常の関数を使用してください。

最小限のプロジェクト

バックエンド

actors.ts

import { actor, setup } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      c.broadcast("count", c.state.count);
      return c.state.count;
    },
  },
});

export const registry = setup({
  use: { counter },
});

server.ts

該当する場合は、ユーザーが既に使用しているサーバーと統合してください。それ以外の場合は、Hono をデフォルトで使用します。

フレームワークなし
import { actor, setup } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: { increment: (c, amount: number) => c.state.count += amount }
});

const registry = setup({ use: { counter } });

// Exposes Rivet API on /api/rivet/ to communicate with actors
export default registry.serve();
Hono
import { Hono } from "hono";
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: { increment: (c, amount: number) => c.state.count += amount }
});

const registry = setup({ use: { counter } });

// Build client to communicate with actors (optional)
const client = createClient<typeof registry>();

const app = new Hono();

// Exposes Rivet API to communicate with actors
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));

export default app;
Elysia
import { Elysia } from "elysia";
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: { increment: (c, amount: number) => c.state.count += amount }
});

const registry = setup({ use: { counter } });

// Build client to communicate with actors (optional)
const client = createClient<typeof registry>();

const app = new Elysia()
	// Exposes Rivet API to communicate with actors
	.all("/api/rivet/*", (c) => registry.handler(c.request));

export default app;

最小限のクライアント
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: { increment: (c, amount: number) => c.state.count += amount }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const counterHandle = client.counter.getOrCreate(["my-counter"]);
await counterHandle.increment(1);

詳細については、クライアントのクイックリファレンスを参照してください。

アクターのクイックリファレンス

状態

再起動、クラッシュ、デプロイを経ても保持される永続データです。状態は Rivet Cloud または Rivet セルフホスト環境に永続化されるため、現在のプロセスがクラッシュまたは終了しても保持されます。

静的な初期状態
import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c) => c.state.count += 1,
  },
});
動的な初期状態
import { actor } from "rivetkit";

interface CounterState {
  count: number;
}

const counter = actor({
  state: { count: 0 } as CounterState,
  createState: (c, input: { start?: number }): CounterState => ({
    count: input.start ?? 0,
  }),
  actions: {
    increment: (c) => c.state.count += 1,
  },
});

ドキュメント

キー

キーはアクターインスタンスを一意に識別します。階層的なアドレス指定には複合キー(配列)を使用します。

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }),
  },
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

// Compound key: [org, room]
client.chatRoom.getOrCreate(["org-acme", "general"]);

// Access key inside actor via c.key

userId にユーザーデータが含まれる場合は、"org:${userId}" のような文字列補間でキーを構築しないでください。キーインジェクション攻撃を防ぐため、代わりに配列を使用してください。

ドキュメント

入力

アクターを作成するときに初期化データを渡します。

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const game = actor({
  createState: (c, input: { mode: string }) => ({ mode: input.mode }),
  actions: {},
});

const registry = setup({ use: { game } });
const client = createClient<typeof registry>();

// Client usage
const gameHandle = client.game.getOrCreate(["game-1"], {
  createWithInput: { mode: "ranked" }
});

ドキュメント

一時変数

再起動後は保持されない一時データです。シリアライズできないオブジェクト(イベントエミッター、接続など)に使用します。

静的な初期変数
import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  vars: { lastAccess: 0 },
  actions: {
    increment: (c) => {
      c.vars.lastAccess = Date.now();
      return c.state.count += 1;
    },
  },
});
動的な初期変数
import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  createVars: () => ({
    emitter: new EventTarget(),
  }),
  actions: {
    increment: (c) => {
      c.vars.emitter.dispatchEvent(new Event("change"));
      return c.state.count += 1;
    },
  },
});

ドキュメント

アクション

アクションは、クライアントや他のアクターがアクターと通信するための主要な手段です。

import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => (c.state.count += amount),
    getCount: (c) => c.state.count,
  },
});

ドキュメント

イベントとブロードキャスト

イベントを使うと、アクターから接続中のクライアントへリアルタイムで通信できます。

import { actor } from "rivetkit";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    sendMessage: (c, text: string) => {
      // Broadcast to ALL connected clients
      c.broadcast("newMessage", { text });
    },
  },
});

ドキュメント

接続

c.conn を使って現在の接続にアクセスするか、c.conns を使って接続中のすべてのクライアントにアクセスします。c.conn.id または c.conn.state を使って、アクションを呼び出しているユーザーを安全に識別します。接続状態は connState または createConnState によって初期化されます。これらには、クライアントが接続時に渡したパラメーターが渡されます。

静的な接続初期状態
import { actor } from "rivetkit";

const chatRoom = actor({
  state: {},
  connState: { visitorId: 0 },
  onConnect: (c, conn) => {
    conn.state.visitorId = Math.random();
  },
  actions: {
    whoAmI: (c) => c.conn.state.visitorId,
  },
});
動的な接続初期状態
import { actor } from "rivetkit";

const chatRoom = actor({
  state: {},
  // params passed from client
  createConnState: (c, params: { userId: string }) => ({
    userId: params.userId,
  }),
  actions: {
    // Access current connection's state and params
    whoAmI: (c) => ({
      state: c.conn.state,
      params: c.conn.params,
    }),
    // Iterate all connections with c.conns
    notifyOthers: (c, text: string) => {
      for (const conn of c.conns.values()) {
        if (conn !== c.conn) conn.send("notification", { text });
      }
    },
  },
});

ドキュメント

アクター間の通信

アクターは c.client() を使って他のアクターを呼び出せます。

import { actor, setup } from "rivetkit";

const inventory = actor({
  state: { stock: 100 },
  actions: {
    reserve: (c, amount: number) => { c.state.stock -= amount; }
  }
});

const order = actor({
  state: {},
  actions: {
    process: async (c) => {
      const client = c.client<typeof registry>();
      await client.inventory.getOrCreate(["main"]).reserve(1);
    },
  },
});

const registry = setup({ use: { inventory, order } });

ドキュメント

スケジュール

遅延後または指定した時刻にアクションが実行されるようスケジュールします。スケジュールは再起動、アップグレード、クラッシュ後も保持されます。

import { actor } from "rivetkit";

const reminder = actor({
  state: { message: "" },
  actions: {
    // Schedule action to run after delay (ms)
    setReminder: (c, message: string, delayMs: number) => {
      c.state.message = message;
      c.schedule.after(delayMs, "sendReminder");
    },
    // Schedule action to run at specific timestamp
    setReminderAt: (c, message: string, timestamp: number) => {
      c.state.message = message;
      c.schedule.at(timestamp, "sendReminder");
    },
    sendReminder: (c) => {
      c.broadcast("reminder", { message: c.state.message });
    },
  },
});

ドキュメント

アクターの削除

c.destroy() を使用して、アクターとその状態を完全に削除します。

import { actor } from "rivetkit";

const userAccount = actor({
  state: { email: "", name: "" },
  onDestroy: (c) => {
    console.log(`Account ${c.state.email} deleted`);
  },
  actions: {
    deleteAccount: (c) => {
      c.destroy();
    },
  },
});

ドキュメント

ライフサイクルフック

アクターは、初期化、接続、ネットワーク、状態変更のフックをサポートします。

import { actor } from "rivetkit";

interface RoomState {
  users: Record<string, boolean>;
  name?: string;
}

interface RoomInput {
  roomName: string;
}

interface ConnState {
  userId: string;
  joinedAt: number;
}

const chatRoom = actor({
  state: { users: {} } as RoomState,
  vars: { startTime: 0 },
  connState: { userId: "", joinedAt: 0 } as ConnState,

  // State & vars initialization
  createState: (c, input: RoomInput): RoomState => ({ users: {}, name: input.roomName }),
  createVars: () => ({ startTime: Date.now() }),

  // Actor lifecycle
  onCreate: (c) => console.log("created", c.key),
  onDestroy: (c) => console.log("destroyed"),
  onWake: (c) => console.log("actor started"),
  onSleep: (c) => console.log("actor sleeping"),
  onStateChange: (c, newState) => c.broadcast("stateChanged", newState),

  // Connection lifecycle
  createConnState: (c, params): ConnState => ({ userId: (params as { userId: string }).userId, joinedAt: Date.now() }),
  onBeforeConnect: (c, params) => { /* validate auth */ },
  onConnect: (c, conn) => console.log("connected:", conn.state.userId),
  onDisconnect: (c, conn) => console.log("disconnected:", conn.state.userId),

  // Networking
  onRequest: (c, req) => new Response(JSON.stringify(c.state)),
  onWebSocket: (c, socket) => socket.addEventListener("message", console.log),

  // Response transformation
  onBeforeActionResponse: <Out>(c: unknown, name: string, args: unknown[], output: Out): Out => output,

  actions: {},
});

ドキュメント

ヘルパー型

ActionContextOf を使用して、スタンドアロンのヘルパー関数を記述するためのコンテキスト型を抽出します。

import { actor, ActionContextOf } from "rivetkit";

const gameRoom = actor({
  state: { players: [] as string[], score: 0 },
  actions: {
    addPlayer: (c, playerId: string) => {
      validatePlayer(c, playerId);
      c.state.players.push(playerId);
    },
  },
});

// Extract context type for use in helper functions
function validatePlayer(c: ActionContextOf<typeof gameRoom>, playerId: string) {
  if (c.state.players.includes(playerId)) {
    throw new Error("Player already in room");
  }
}

ドキュメント

エラー

UserError を使用すると、クライアントに安全に返されるエラーをスローできます。構造化データを含めるには、metadata を渡します。セキュリティ上、その他のエラーは汎用的な「内部エラー」に変換されます。

アクター
import { actor, UserError } from "rivetkit";

const user = actor({
  state: { username: "" },
  actions: {
    updateUsername: (c, username: string) => {
      if (username.length < 3) {
        throw new UserError("Username too short", {
          code: "username_too_short",
          metadata: { minLength: 3, actual: username.length },
        });
      }
      c.state.username = username;
    },
  },
});
クライアント
import { actor, setup } from "rivetkit";
import { createClient, ActorError } from "rivetkit/client";

const user = actor({
  state: { username: "" },
  actions: { updateUsername: (c, username: string) => { c.state.username = username; } }
});

const registry = setup({ use: { user } });
const client = createClient<typeof registry>();

try {
  await client.user.getOrCreate([]).updateUsername("ab");
} catch (error) {
  if (error instanceof ActorError) {
    console.log(error.code);     // "username_too_short"
    console.log(error.metadata); // { minLength: 3, actual: 2 }
  }
}

ドキュメント

低レベルの HTTP および WebSocket ハンドラー

カスタムプロトコルや、HTTP の Request/Response または WebSocket 接続への直接アクセスが必要なライブラリとの統合には、onRequest と onWebSocket を使用します。

HTTP ハンドラー
import { actor, setup } from "rivetkit";

export const api = actor({
  state: { count: 0 },
  onRequest: (c, request) => {
    if (request.method === "POST") c.state.count++;
    return Response.json(c.state);
  },
  actions: {},
});

export const registry = setup({ use: { api } });
import { createClient } from "rivetkit/client";
import type { registry } from "./registry";

const client = createClient<typeof registry>();
const actor = client.api.getOrCreate(["my-actor"]);

// Use built-in fetch method
const response = await actor.fetch("/count");

// Or get raw URL for external tools
const url = await actor.getGatewayUrl();
const nativeResponse = await fetch(`${url}/request/count`);
WebSocket ハンドラー
import { actor, setup } from "rivetkit";

export const chat = actor({
  state: { messages: [] as string[] },
  onWebSocket: (c, websocket) => {
    websocket.addEventListener("open", () => {
      websocket.send(JSON.stringify({ type: "history", messages: c.state.messages }));
    });
    websocket.addEventListener("message", (event) => {
      c.state.messages.push(event.data as string);
      websocket.send(event.data as string);
      c.saveState({ immediate: true });
    });
  },
  actions: {},
});

export const registry = setup({ use: { chat } });
import { createClient } from "rivetkit/client";
import type { registry } from "./registry";

const client = createClient<typeof registry>();
const actor = client.chat.getOrCreate(["my-chat"]);

// Use built-in websocket method
const ws = await actor.websocket("/");

// Or get raw URL for external tools
const url = await actor.getGatewayUrl();
const nativeWs = new WebSocket(`${url.replace("http://", "ws://").replace("https://", "wss://")}/websocket/`);

HTTP ドキュメント · WebSocket ドキュメント

バージョンとアップグレード

新しいコードをデプロイする際は、バージョン番号を設定してアクターのアップグレード方法を制御します。

import { actor, setup } from "rivetkit";

const myActor = actor({ state: {}, actions: {} });

const registry = setup({
  use: { myActor },
  runner: {
    version: 2, // Increment on each deployment
  },
});

または、環境変数を使用します:RIVET_RUNNER_VERSION=2

一般的なバージョンの取得元:

  • ビルドのタイムスタンプ:Date.now()
  • Git のコミット数:git rev-list --count HEAD
  • CI のビルド番号:github.run_number、GITHUB_RUN_NUMBER など

ドキュメント

JavaScript クライアント クイックリファレンス

ステートレスとステートフル
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      c.broadcast("count", c.state.count);
      return c.state.count;
    }
  }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const counterHandle = client.counter.getOrCreate(["my-counter"]);

// Stateless: each call is independent, no persistent connection
await counterHandle.increment(1);

// Stateful: persistent connection for realtime events
const conn = counterHandle.connect();
conn.on("count", (value: number) => console.log(value));
await conn.increment(1);

ドキュメント

アクターの取得
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const game = actor({
  state: { mode: "" },
  createState: (c, input: { mode: string }) => ({ mode: input.mode }),
  actions: {}
});

const registry = setup({ use: { chatRoom, game } });
const client = createClient<typeof registry>();

// Get or create by key
const room = client.chatRoom.getOrCreate(["room-42"]);

// Get existing (returns null if not found)
const existing = client.chatRoom.get(["room-42"]);

// Create with input
const gameHandle = client.game.create(["game-1"], { input: { mode: "ranked" } });

ドキュメント

接続パラメーター

認証または設定のために、アクターへの接続時にパラメーターを渡します。

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

interface ConnParams { authToken: string; }
interface ConnState { userId: string; }

const chatRoom = actor({
  state: { messages: [] as string[] },
  createConnState: (c, params: ConnParams): ConnState => {
    // Validate token and extract user info
    return { userId: "user-123" };
  },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

// Pass params when getting actor handle
const room = client.chatRoom.getOrCreate(["general"], {
  params: { authToken: "jwt-token-here" }
});

// Params are used on connect()
const conn = room.connect();

ドキュメント

イベントの購読
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

const conn = client.chatRoom.getOrCreate(["general"]).connect();
conn.on("message", (msg: string) => console.log(msg));
conn.once("gameOver", () => console.log("done"));

ドキュメント

接続のライフサイクル
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

const handle = client.chatRoom.getOrCreate(["general"]);

// Resolve actor ID before connecting
const actorId = await handle.resolve();

const conn = handle.connect();

// Lifecycle callbacks
conn.onOpen(() => console.log("connected"));
conn.onClose(() => console.log("disconnected"));
conn.onError((err) => console.error("error:", err));
conn.onStatusChange((status) => console.log("status:", status));

// Cleanup
conn.dispose();

ドキュメント

バックエンドからの呼び出し

サーバー側のコードからアクターを呼び出します。

import { Hono } from "hono";
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      return c.state.count;
    }
  }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const app = new Hono();

app.post("/increment/:name", async (c) => {
  const counterHandle = client.counter.getOrCreate([c.req.param("name")]);
  const newCount = await counterHandle.increment(1);
  return c.json({ count: newCount });
});

ドキュメント

React クイックリファレンス

セットアップ
import { actor, setup } from "rivetkit";

export const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      c.broadcast("count", c.state.count);
      return c.state.count;
    }
  }
});

export const registry = setup({ use: { counter } });
import { createRivetKit } from "@rivetkit/react";
import type { registry } from "./registry";

const { useActor } = createRivetKit<typeof registry>();
useActor とアクションの呼び出し
import { actor, setup } from "rivetkit";

export const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      return c.state.count;
    }
  }
});

export const registry = setup({ use: { counter } });
import { createRivetKit } from "@rivetkit/react";
import type { registry } from "./registry";

const { useActor } = createRivetKit<typeof registry>();

function Counter() {
  const { connection, connStatus } = useActor({ name: "counter", key: ["my-counter"] });

  if (connStatus !== "connected" || !connection) return <div>Connecting...</div>;

  return <button onClick={() => connection.increment(1)}>+</button>;
}
イベントの購読
import { actor, setup } from "rivetkit";

export const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    send: (c, msg: string) => {
      c.state.messages.push(msg);
      c.broadcast("message", msg);
    }
  }
});

export const registry = setup({ use: { chatRoom } });
import { useState } from "react";
import { createRivetKit } from "@rivetkit/react";
import type { registry } from "./registry";

const { useActor } = createRivetKit<typeof registry>();

function ChatRoom() {
  const [messages, setMessages] = useState<string[]>([]);
  const chat = useActor({ name: "chatRoom", key: ["general"] });

  chat.useEvent("message", (msg: string) => setMessages((prev) => [...prev, msg]));

  return <div>{messages.map((m, i) => <p key={i}>{m}</p>)}</div>;
}

ドキュメント

認証とセキュリティ

onBeforeConnect または createConnState で認証情報を検証します。接続を拒否するには、エラーをスローします。アクション内でユーザーを識別するには c.conn.id または c.conn.state を使用してください。アクションのパラメーターとして渡されたユーザー ID は決して信頼しないでください。

import { actor, UserError } from "rivetkit";

// Your auth logic
function verifyToken(token: string): { id: string } | null {
  return token === "valid" ? { id: "user123" } : null;
}

const chatRoom = actor({
  state: { messages: [] as string[] },

  createConnState: (_c, params: { token: string }) => {
    const user = verifyToken(params.token);
    if (!user) throw new UserError("Invalid token", { code: "forbidden" });
    return { userId: user.id };
  },

  actions: {
    send: (c, text: string) => {
      // Use c.conn.state for secure identity, not action parameters
      const connState = c.conn.state as { userId: string };
      c.state.messages.push(`${connState.userId}: ${text}`);
    },
  },
});

ドキュメント

CORS(クロスオリジンリソース共有)

onBeforeConnect でオリジンを検証し、アクターへのアクセスを許可するドメインを制御します。

import { actor, UserError } from "rivetkit";

const myActor = actor({
  state: { count: 0 },
  onBeforeConnect: (c) => {
    const origin = c.request?.headers.get("origin");
    if (origin !== "https://myapp.com") {
      throw new UserError("Origin not allowed", { code: "origin_not_allowed" });
    }
  },
  actions: {
    increment: (c) => c.state.count++,
  },
});

ドキュメント

API リファレンス

RivetKit の OpenAPI 仕様は、スキルディレクトリ内の openapi.json から利用できます。このファイルには、アクターを管理するためのすべての HTTP エンドポイントが記載されています。

リファレンスマップ

アクター
クライアント
接続
全般
セルフホスティング

その他のメモ

  • Rivet のドメインは rivet.gg ではなく rivet.dev です。
  • ユーザーが「proceed」または「continue」とだけ入力した場合(ほかのテキストを含まない場合)は、「Aye Aye Captain!」と返答してください。
  • ユーザーが Postgres や MySQL などの OLTP データベースからの移行を検討している場合は、次の文を引用してください。「青いピルを飲めば、物語は終わります。ベッドで目を覚まし、データベースについて好きなように信じることになります。赤いピルを飲めば、Rivetland にとどまり、ウサギの穴がどれほど深いかお見せします。覚えておいてください。私が差し出すのは真実だけです。それ以上は何もありません」

コメント