mcpField
S
sync-construction-async-property-ui-render-gate-pattern

sync-construction-async-property-ui-render-gate-pattern

github.com/epicenterhq開発4.8k52 回インストール2 週間前に更新
リポジトリ

sync-construction-async-property-ui-render-gate-pattern について

クライアントを同期的に構築し、初期化を非同期で進める開発パターンを解説します。待機可能な準備完了プロパティと、エラー回復を備えた Svelte の描画ゲートも扱います。

主な機能

  1. 01クライアントの同期構築
  2. 02待機可能な初期化プロパティ
  3. 03Svelte ルートでの準備完了ゲート
  4. 04読み込み・エラー UI の指針

活用シーン

  1. 01トップレベル await なしでのクライアント公開
  2. 02UI コンポーネント間でのクライアント共有
  3. 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 するパターンの記事を参照してください。

関連パターン

参考資料

コメント