query-layer について
TanStack Query を使い、UI コンポーネントとサービスの間にリアクティブなクエリ層を構成する開発スキルです。共有クエリやミューテーション、キャッシュ、エラー処理、命令的な利用方法を説明します。
主な機能
- 01TanStack Query のアダプターパターン
- 02共有クエリとミューテーション定義
- 03キャッシュキーと無効化の指針
- 04リアクティブ API と命令的 API
- 05タグ付きエラーの引き継ぎ
- 06オペレーション層との境界
活用シーン
- 01UI コンポーネント向け共有クエリの定義
- 02共有キャッシュ識別子を使ったミューテーション
- 03コンポーネント外でのデータ取得や存在確認
- 04意図を明確にしたキャッシュ更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill query-layerインストール後、Claude Code で/query-layerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-query-patterns.mderror-transformation-patterns.mdruntime-dependency-injection.md詳細
クエリレイヤーのパターン
参照リポジトリ
- TanStack Query:データ取得のための非同期状態管理
上流の情報源による根拠の確認
TanStack Query の動作、Svelte アダプターの型、キャッシュ無効化のセマンティクス、楽観的更新、またはミューテーションのライフサイクルコールバックが正確性に影響する場合は、記憶に頼る前に DeepWiki で TanStack/query について焦点を絞った質問をしてください。まず概要を把握し、その後、コードを変更する前に、決定的な詳細をローカルにインストールされた型、ソースコード、または公式ドキュメントで確認してください。
安定した基本事項や、以下にすでに記載されているリポジトリ内のパターンについては、DeepWiki を使う必要はありません。
クエリレイヤーは、UI コンポーネントとサービスレイヤーをつなぐリアクティブな橋渡しです。TanStack Query と Wellcrafted のファクトリーを使用して、サービス関数またはオブザーバブルな操作をキャッシュ、ミューテーションのライフサイクル状態、無効化、直接的な命令型アクセスでラップします。
関連スキル:クエリが利用するサービスレイヤーについては
services-layerを参照してください。Svelte 固有の TanStack Query パターンについてはsvelteを参照してください。Result が UI 境界に到達した後のトースト/レポートのパターンについてはerror-handlingを参照してください。
コアアーキテクチャ
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ UI │ --> │ Query │ --> │ Services │
│ Components │ │ Layer │ │ (UI-free) │
└─────────────┘ └─────────────┘ └──────────────┘
↑ │
└────────────────────┘
Reactive Updates
クエリレイヤーの責務:
- 注入された設定/構成を使ってサービスを呼び出す
- アダプターによって新たなローカルエラーが発生しない限り、型付けされたサービスおよび操作のエラーを維持する
- 楽観的更新のために TanStack Query のキャッシュを管理する
- 共有定義用のフック対応
.optionsと、存在する場合は明示的な命令型 API を提供する - エクスポートされた
*Keysマップを通じて共有キャッシュの識別情報を管理する
Wellcrafted クエリ API の形式
| スコープ | クエリ | ミューテーション |
|---|---|---|
| フックローカルの Result アダプター | resultQueryOptions(input) | resultMutationOptions(input) |
| 再利用可能な定義 | defineQuery(input) | defineMutation(input) |
Result を返す関数を TanStack の data/error チャネルに渡す必要があり、命令型 API も共有クエリ識別情報も必要ない場合は、1 つのフック呼び出し箇所で resultQueryOptions と resultMutationOptions を使用してください。
アダプターは内部でアンラップします(Ok.data が返され、Err.error がスローされます)。
Result を返す関数を渡してください。先に unwrap してはいけません。
共有の $lib/queries モジュールでは defineQuery と defineMutation を使用してください。
クエリは .options、.fetch()、.ensure() を公開します。呼び出し可能ではありません。
ミューテーションは .options を公開し、呼び出し可能です。.execute() は公開しません。
Whispering の標準的なクエリモジュール形式
以下の例の元になった Whispering のクエリモジュールは、Whispering がクエリレイヤーを持たないスタンドアロンのファイル/フォルダーアプリになった際に削除されました。この形式は、クエリレイヤーを持つアプリの $lib/queries モジュールにおける規約として、引き続き使われています。
Whispering 形式の $lib/queries モジュールでは、実装対象に近い場所に、信頼できる情報源となる宣言を配置してください。ファクトリーには、セッションが所有するランタイムを明示的に渡します。
export const audioKeys = defineKeys({
availability: (id: string, blobId: string, uploadedAt: string | null) =>
['audio', 'availability', id, blobId, uploadedAt] as const,
});
export function createAudioQueries({ defineQuery }: WhisperingQueryRuntime) {
return {
availability: (recording: Accessor<Recording>) =>
defineQuery({
queryKey: audioKeys.availability(
recording().id,
recording().audioBlobId,
recording().uploadedAt,
),
queryFn: () => getRecordingAudioAvailability(recording()),
}),
};
}
ルール:
- 作業を所有するアダプターまたは状態モジュールの隣に
*Keys = defineKeys({ ... })をエクスポートしてください。 - 静的なキーに
as constは不要です。キーのファクトリーでは、リテラルの位置が重要な場合にas constを使用してください。 - 別のレイヤーで同じフォールバック識別情報が必要でない限り、キーはそれを所有するモジュールに置いてください。
- 小規模で一度しか使わない入力オブジェクトはインライン化してください。入力型を再利用する場合、エクスポートする場合、関数の理解を妨げるほど大きい場合、またはドメイン上の意味を持つ場合に限り、名前付き入力型を定義してください。名前付き入力型は、それを使用するアダプターネームスペースの直前に置いてください。
- 別のモジュールでその型のユニオンを明示する必要がない限り、アダプター固有の
defineErrorsネームスペースはローカルに置いてください。
アダプターの境界:クエリとオペレーション
共有の TanStack オブザベーションサーフェスとして $lib/queries を使用します。直接サービス/状態を呼び出す処理をラップすることも、UI で共有のミューテーション ID(複数の利用箇所、キャッシュ無効化、楽観的更新、useIsMutating、またはそのオペレーションに対する名前付きミューテーションキー)が必要な場合に、$lib/operations のエントリーポイントをラップすることもできます。
オーケストレーションは $lib/operations に置きます。これには、配信、サウンド、分析、クリップボードへの書き込み、複数ステップのワークフローが含まれます。再利用可能なオペレーションはその結果を返します。終了時のインタラクションコントローラーも、その結果を報告できます。ローカルの保留状態を監視するためだけに、単一コンポーネント専用のオペレーションを $lib/queries に移さないでください。コンポーネントでローカルの createMutation と直接の await のどちらを選ぶかは、svelte スキルが扱います。
依存関係の方向
UI -> operations/* -> services/* + state/* + $lib/tauri
UI -> queries/* -> services/* or operations/*, plus narrow state reads/writes for observed lifecycle
クエリモジュールは、セッションが所有するクエリランタイムを受け取り、サービス、状態、またはオペレーションをインポートします。処理を順序付けるためだけに、兄弟クエリモジュールをインポートしてはいけません。アダプターをまたぐ調整はオペレーションに置きます。
エラーの流れ
Whispering では、サービスとオペレーションのエラーにはすでにタグが付いています。クエリアダプターはエラーをそのまま渡します。どのように表示するかは、UI/報告の境界で決定します。
Service / Operation -> Query Adapter -> UI / Report
TaggedError<'Name'> same error report.error({ cause: error })
アダプター自体が、下位レイヤーで処理できない失敗を検出した場合に限り、クエリ固有のエラーを定義します。たとえば、オペレーションを呼び出す前に録音の検索で見つからない場合です。
リアクティブな使用と命令的な使用
クエリレイヤーのアダプターは、リアクティブなフックの使用と明示的な命令的な使用を提供します。
リアクティブインターフェイス:.options
共有クエリアダプターは、.options を静的オブジェクトとして公開します。Svelte のフックでは、アクセサー内で読み取ります。
<script lang="ts">
import { createQuery, createMutation } from '@tanstack/svelte-query';
import { getWhisperingQueries } from '$lib/whispering/context';
const queries = getWhisperingQueries();
const availability = createQuery(() =>
queries.audio.availability(() => recording).options,
);
const transcribeRecording = createMutation(
() => queries.transcription.transcribeRecording.options,
);
</script>
{#if availability.isPending}
<Spinner />
{:else if availability.error !== null}
<Error message={availability.error.message} />
{:else}
<AvailabilityBadge value={availability.data} />
{/if}
命令的インターフェイス:クエリではキャッシュポリシーを選択し、ミューテーションは呼び出し可能
コンポーネントのコンテキスト外で使用する場合、または呼び出し側が直接 Result を必要とする場合:
// In an event handler or workflow
async function handleDownload(recording: Recording) {
const { error } = await queries.download.downloadRecording(recording);
if (error !== null) {
report.error({ cause: error });
return;
}
report.success({ title: 'Recording downloaded' });
}
// In a sequential workflow
async function stopAndTranscribe(toastId: string) {
const { data: url, error: playbackUrlError } =
await queries.audio.availability(() => recording).fetch();
if (playbackUrlError !== null) {
report.error({ cause: playbackUrlError });
return;
}
// Continue with transcription...
}
TanStack にクエリの通常の古さのポリシーを評価させる場合は、.fetch() を使用します。キャッシュされたデータが新鮮であれば、リクエストなしで返されることがあります。キャッシュデータがあればそれを使用し、キャッシュが空の場合にのみフェッチすればよい場合は、.ensure() を使用します。
使い分け
| アダプターの種類 | パターン |
|---|---|
| 共有リアクティブクエリ | createQuery(() => queries.thing.options) |
| 共有リアクティブミューテーション | createMutation(() => queries.thing.options) |
| 命令的なクエリ読み取り | queries.thing(...).fetch() または queries.thing(...).ensure() |
| 命令的なミューテーション | queries.thing(input) |
ローカルコンポーネントでのオペレーションの配置やライフサイクルの判断については、
svelte スキルのミューテーションに関するガイダンスを参照してください。
主なルール
- 共有キャッシュ ID には
defineKeysを使用する - キーマップを所有者の隣でエクスポートします .options(括弧なし)を使用する - 静的オブジェクトなので、Svelte ではアクセサーで囲みます- タグ付きエラーを既定では変換しない - サービス/オペレーションのエラーを報告の境界までそのまま渡します
- サービスにはアプリの入力を明示的に渡す - 利用側の境界で設定とデバイス構成を注入します
- コンポーネントのライフサイクルポリシーは
svelteに置く - このスキルでは共有アダプターの形状とキャッシュの動作を扱います - キャッシュは意図を持って更新する - キャッシュの所有者とロールバック経路が明確な場合に限って楽観的書き込みを使用し、それ以外の場合は無効化するか再フェッチします
参考資料
作業内容に応じて、以下を必要に応じて参照してください。
-
エラーをそのまま渡す例とアンチパターンを扱う場合は、references/error-transformation-patterns.md を読みます
-
ランタイム依存関係の注入とサービスの選択を扱う場合は、references/runtime-dependency-injection.md を読みます
-
キャッシュ管理、クエリ定義、RPC 名前空間、通知の調整を扱う場合は、references/advanced-query-patterns.md を読みます
-
サービスの実装方法については、
services-layerスキルを参照してください -
trySync/tryAsync のパターンとエラー時のトースト表示の規約については、
error-handlingスキルを参照してください