sync-construction-async-property-ui-render-gate-pattern について
クライアントを同期的に構築し、初期化を非同期で進める開発パターンを解説します。待機可能な準備完了プロパティと、エラー回復を備えた Svelte の描画ゲートも扱います。
主な機能
- 01クライアントの同期構築
- 02待機可能な初期化プロパティ
- 03Svelte ルートでの準備完了ゲート
- 04読み込み・エラー UI の指針
活用シーン
- 01トップレベル await なしでのクライアント公開
- 02UI コンポーネント間でのクライアント共有
- 03準備完了までの Svelte 描画制御
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill sync-construction-async-property-ui-render-gate-patternインストール後、Claude Code で/sync-construction-async-property-ui-render-gate-patternを入力して読み込まれたか確認します。
詳細
同期的な構築、非同期プロパティ
クライアントの初期化は同期的に行います。非同期処理は、参照を渡しながら await できるプロパティとして保持します。
このパターンを適用する場面
次のような場合に使用します。
- クライアントの初期化が非同期の場合(IndexedDB、サーバー接続、ファイルシステム)
awaitなしでインポート可能にする必要があるモジュールエクスポート- クライアントに同期的にアクセスしたい UI コンポーネント
- 準備が整うまでレンダリングを保留したい SvelteKit アプリ
非同期構築で問題が起きている兆候:
- あらゆる場所に
await getX()パターンがある - バンドラーからトップレベル await に関する警告が出る
- シングルトンへのアクセスをラップするゲッター関数がある
- クライアントを直接インポートできないコンポーネントがある
問題
非同期コンストラクターはエクスポートできません。
// This doesn't work
export const client = await createClient(); // Top-level await breaks bundlers
そのため、ゲッターパターンを使うことになります。
let client: Client | null = null;
export async function getClient() {
if (!client) {
client = await createClient();
}
return client;
}
// Every consumer must await
const client = await getClient();
呼び出し側では毎回 await が必要です。オブジェクトではなく、Promise を受け渡すことになります。
パターン
構築は同期的に行います。非同期処理をオブジェクトに追加します。
// client.ts
export const client = createClient();
// Sync access works immediately
client.save(data);
client.load(id);
// Await the async work when you need to
await client.whenSynced;
構築はすぐに完了します。ディスクからの読み込みやサーバーへの接続などの非同期初期化はバックグラウンドで実行され、whenSynced で追跡されます。
UI のレンダリングゲート
Svelte では、ルートで一度だけゲートを設けます。ローディング状態には @epicenter/ui/spinner を、エラーからの復旧には @epicenter/ui/empty を使用します。
<script>
import * as Empty from '@epicenter/ui/empty';
import { Spinner } from '@epicenter/ui/spinner';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
import { client } from '$lib/client';
</script>
{#await client.whenSynced}
<Empty.Root class="flex-1">
<Empty.Media>
<Spinner class="size-5 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Loading…</Empty.Title>
</Empty.Root>
{:then}
{@render children?.()}
{:catch}
<Empty.Root class="flex-1">
<Empty.Media>
<TriangleAlertIcon class="size-8 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Failed to load</Empty.Title>
<Empty.Description>
Something went wrong during initialization. Try reloading.
</Empty.Description>
</Empty.Root>
このゲートにより、子コンポーネントのスクリプトが実行される時点で、非同期処理が完了していることが保証されます。子コンポーネントは準備状態を確認せずに、同期的にアクセスできます。
必ず {:catch} を含めてください:非同期シードに失敗した場合(たとえば browser.windows.getAll が例外をスローした場合)、無限にスピナーが表示されるのではなく、ユーザーに対応可能なエラーが表示されます。
実装
withCapabilities() のフルーエントビルダーは、同期的に構築されたオブジェクトに非同期処理を追加します。
function createClient() {
const state = initializeSyncState();
return {
save(data) {
/* sync method */
},
load(id) {
/* sync method */
},
withCapabilities({ persistence }) {
const whenSynced = persistence(state);
return Object.assign(this, { whenSynced });
},
};
}
// Usage
export const client = createClient().withCapabilities({
persistence: (state) => loadFromIndexedDB(state),
});
変更前と変更後
| 項目 | 非同期構築 | 同期構築+whenSynced |
|---|---|---|
| モジュールエクスポート | 直接エクスポートできない | オブジェクトをエクスポート |
| 利用側のコード | あらゆる場所で await getX() | 直接インポートして同期的に使用 |
| UI との統合 | Promise の処理が煩雑 | 単一の {#await} ゲート |
| 型シグネチャ | Promise<X> | .whenSynced を持つ X |
実際の例:y-indexeddb
Yjs エコシステムでは、このパターンが広く使われています。
const provider = new IndexeddbPersistence('my-db', doc);
// Constructor returns immediately
provider.on('update', handleUpdate); // Sync access works
await provider.whenSynced; // Wait when you need to
構築をブロックすることはありません。非同期処理は常に、await できるプロパティに延期されます。
別のパターン:各メソッド内で await する
別の方法として、whenReady プロパティを完全に省き、各メソッド内に初期化の await を隠すこともできます。代表的な例は idb です。
const dbPromise = openDB('keyval-store', 1, { upgrade(db) { db.createObjectStore('keyval') } });
export async function get(key) { return (await dbPromise).get('keyval', key); }
export async function set(key, val) { return (await dbPromise).put('keyval', val, key); }
初期化済みの状態に依存する同期メソッドをクライアントが持つ場合は、whenReady を使用します。すべてのメソッドがもともと非同期の場合(データベースアクセスなど)は、各メソッド内で await する方法を使用します。詳しい比較については、idb の各メソッド内で await するパターンの記事を参照してください。
関連パターン
- 遅延シングルトン:競合状態を防ぎながら遅延初期化する必要がある場合
- 並列マップを使用しない:状態を個別に追跡せず、インスタンスに追加する方法
参考資料
- 記事全文:図を交えた詳細な説明
- 包括的なガイド:idb の例を含む、480 行の詳細な解説
- idb の各メソッド内で await するパターン:純粋な非同期 API 向けの関連パターン