クライアント接続の管理
バックエンドから、クライアントの接続・切断をリアルタイムに検知して応答します。
クライアント接続を使う理由
ユーザーがオンラインかどうかを知ることに依存する機能を作っているとします。たとえば、プレゼンス表示のあるチャットアプリ、アクティブ参加者を追跡する共同編集エディタ、接続中プレイヤーを表示するゲームロビーなどです。
接続追跡がない場合、ハートビートのポーリング(polling)や、独自の WebSocket ロジックを実装する必要があります。Squid の client connection handler を使えば、単一の関数にデコレータ(decorator)を付けるだけで、状態変化が起きた瞬間に反応できます。
// Backend: react to connection changes
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
if (state === 'DISCONNECTED') {
await this.squid.collection('presence').doc(clientId).update({ status: 'offline' });
}
}
// Frontend: check connection status
const isConnected = squid.connectionDetails().connected;
ポーリング不要。カスタム WebSocket 管理も不要。必要なのはデコレータだけです。
概要
クライアント接続により、バックエンドはクライアントが Squid サーバーに接続した、切断した、または削除されたタイミングを検知できます。各クライアントには接続時に一意の clientId が割り当てられ、これを使ってプレゼンスの追跡、リソースのクリーンアップ、ワークフローのトリガーなどを行えます。
クライアント接続を使うべきとき
| ユースケース | 推奨 |
|---|---|
| ユーザーのオンライン/オフライン状態を追跡する | クライアント接続 |
| ユーザー離脱時にリソースをクリーンアップする | クライアント接続 |
| データベース変更に反応する | Triggers を使用 |
| スケジュール実行する | Schedulers を使用 |
仕組み
- クライアントが Squid に接続し、一意の
clientIdが割り当てられる - バックエンドの handler が state
CONNECTEDで呼び出される - クライアントが切断すると(例: ブラウザタブを閉じる)、handler が
DISCONNECTEDで呼び出される - 一定期間後、クライアントが再接続しない場合、Squid は
clientIdを削除し、handler をREMOVEDで呼び出す - 削除後にクライアントが再接続した場合、新しい
clientIdが発行される
クイックスタート
前提条件
squid initで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがNPMからインストールされていること
Step 1: 接続ハンドラーを作成する
サービスクラスに connection state handler を追加します。
import { SquidService, clientConnectionStateHandler } from '@squidcloud/backend';
import { ClientConnectionState, ClientId } from '@squidcloud/client';
export class ExampleService extends SquidService {
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
console.log(`Client ${clientId} is now ${state}`);
}
}
Step 2: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカルで実行します。
squid start
クラウドへデプロイする場合は、deploying your backend を参照してください。
Step 3: クライアントで接続状態を監視する
import { filter } from 'rxjs';
// Check if currently connected
const isConnected = squid.connectionDetails().connected;
// Get the current client's ID
const clientId = squid.connectionDetails().clientId;
// React to connection changes
squid
.connectionDetails()
.observeConnected()
.pipe(filter(Boolean))
.subscribe(() => {
console.log('Connected with client ID:', squid.connectionDetails().clientId);
});
コアコンセプト
接続状態(Connection states)
各クライアントは次の状態へ遷移します。
| State | 意味 |
|---|---|
CONNECTED | クライアントがサーバーに接続した直後 |
DISCONNECTED | クライアントは切断したが、再接続に備えて Squid が clientId を保持している状態 |
REMOVED | クライアントが十分長く切断されていたため、Squid が clientId を破棄した状態。次回接続では新しい ID が割り当てられる。 |
Client ID(クライアント ID)
すべてのクライアントは接続時に一意の clientId を受け取ります。この ID は次の特性を持ちます。
- フロントエンドでは
squid.connectionDetails().clientIdから参照可能 - バックエンドでは
this.context.clientIdから参照可能(接続ハンドラーだけでなく任意のバックエンド関数内) - 短時間の切断に対しては安定(
REMOVEDになるまで同じ ID が保持される) - 認証プロバイダの
userIdとは別物。必要なら両者のマッピングが必要です。
@clientConnectionStateHandler デコレータ
デコレータが付いた関数は 2 つの引数を受け取ります。
| パラメータ | 型 | 説明 |
|---|---|---|
clientId | ClientId (string) | 状態が変化したクライアントの ID |
connectionState | ClientConnectionState | 'CONNECTED'、'DISCONNECTED'、'REMOVED' のいずれか |
関数は void または Promise<void> を返せます。
フロントエンド接続 API
接続情報は squid.connectionDetails() から参照します。
| プロパティ / メソッド | 戻り値の型 | 説明 |
|---|---|---|
.connected | boolean | クライアントが現在接続中かどうか |
.clientId | string | この接続に割り当てられた一意のクライアント ID |
.observeConnected() | Observable<boolean> | 接続時に true、切断時に false を emit する |
Squid client は WebSocket 接続を遅延(lazy)で確立します。つまり、操作が必要になったときにのみサーバーへ接続します(例: .snapshots() でクエリを subscribe するなど)。.connected や .observeConnected() を呼ぶだけでは接続は開始されません。状態確認の前に必ず接続されている必要がある場合は、まずデータを subscribe するか、接続をトリガーする操作を実行してください。
エラーハンドリング
ハンドラーのエラー
@clientConnectionStateHandler 関数がエラーを throw すると、サーバー側でログに記録されますが、クライアントの接続には影響しません。サイレントな失敗を避けるため、ハンドラー内でエラーを処理してください。
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
try {
await this.updatePresence(clientId, state);
} catch (error) {
console.error(`Failed to update presence for ${clientId}:`, error);
}
}
よくある問題
| 問題 | 原因 | 解決策 |
|---|---|---|
| Handler が呼ばれない | Service が export されていない、または backend がデプロイされていない | service/index.ts で service を export し、squid deploy を実行する |
clientId が予期せず変わる | 長時間の切断後に client が removed された | REMOVED 状態でクリーンアップし、CONNECTED で再マップする |
observeConnected() が throw する | client が passive mode になっている | Squid client が active(デフォルト)モードで初期化されていることを確認する |
ベストプラクティス
- 接続時に
clientIdをuserIdにマップする。clientIdは接続スコープ、userIdは認証スコープのため、クライアント接続時にマッピングを作成しておくと、接続からユーザーを引けます。 DISCONNECTEDだけでなくREMOVEDでクリーンアップする。DISCONNECTEDのクライアントは同じclientIdで再接続する可能性があります。状態がREMOVEDのときだけリソースを削除してください。- ハンドラーのロジックは高速に保つ。 connection state handler は全クライアントの接続/切断ごとに実行されます。高コストな処理は避けるか、非同期で連鎖させてください。
- authentication を使ってユーザーを特定する。
clientIdだけではユーザーが誰か分かりません。プレゼンス機能は認証と組み合わせて構築してください。
コード例
ユーザープレゼンスの追跡
この例では、presence コレクションで clientId を userId にマッピングすることで、どのユーザーがオンラインかを追跡します。
Frontend: 接続時にプレゼンスを登録
import { filter } from 'rxjs';
interface PresenceData {
userId: string;
status: 'online' | 'offline';
}
// When connected, insert a presence record linking clientId to userId
squid
.connectionDetails()
.observeConnected()
.pipe(filter(Boolean))
.subscribe(() => {
const clientId = squid.connectionDetails().clientId;
squid
.collection<PresenceData>('presence')
.doc(clientId)
.insert({ userId: currentUserId, status: 'online' });
});
Backend: 切断/削除時にプレゼンスを更新
import { SquidService, clientConnectionStateHandler } from '@squidcloud/backend';
import { ClientConnectionState, ClientId } from '@squidcloud/client';
export class PresenceService extends SquidService {
@clientConnectionStateHandler()
async handlePresenceChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
const presenceRef = this.squid.collection('presence').doc(clientId);
if (state === 'DISCONNECTED') {
await presenceRef.update({ status: 'offline' });
} else if (state === 'REMOVED') {
await presenceRef.delete();
}
}
}
Frontend: オンラインユーザーをクエリする
// Get all online users
const onlineUsers = await squid
.collection<PresenceData>('presence')
.query()
.eq('status', 'online')
.dereference()
.snapshot();
console.log(
'Online users:',
onlineUsers.map((u) => u.userId)
);
クエリについての詳細は Queries を参照してください。
関連項目
- Authentication - バックエンドでユーザーを特定する
- Triggers - データベース変更に反応する
- Database - コレクションのクエリと更新を行う