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

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sync Construction, Async Property

同步构造,异步属性

The initialization of the client is synchronous. The async work is stored as a property you can await, while passing the reference around.
客户端的初始化是同步的。异步操作被存储为一个可等待的属性,同时可以传递对象引用。

When to Apply This Pattern

何时应用此模式

Use this when you have:
  • Async client initialization (IndexedDB, server connection, file system)
  • Module exports that need to be importable without
    await
  • UI components that want sync access to the client
  • SvelteKit apps where you want to gate rendering on readiness
Signals you're fighting async construction:
  • await getX()
    patterns everywhere
  • Top-level await complaints from bundlers
  • Getter functions wrapping singleton access
  • Components that can't import a client directly
当你遇到以下场景时可以使用该模式:
  • 客户端需要异步初始化(如IndexedDB、服务器连接、文件系统)
  • 模块导出的内容无需
    await
    即可导入
  • UI组件需要同步访问客户端
  • 在SvelteKit应用中,希望在准备就绪后再开始渲染
以下信号表明你正在与异步构造作斗争:
  • 到处都是
    await getX()
    的模式
  • 打包工具对顶层
    await
    报错
  • 使用 getter 函数封装单例访问
  • 组件无法直接导入客户端

The Problem

存在的问题

Async constructors can't be exported:
typescript
// This doesn't work
export const client = await createClient(); // Top-level await breaks bundlers
So you end up with getter patterns:
typescript
let client: Client | null = null;

export async function getClient() {
	if (!client) {
		client = await createClient();
	}
	return client;
}

// Every consumer must await
const client = await getClient();
Every call site needs
await
. You're passing promises around instead of objects.
异步构造函数无法直接导出:
typescript
// 这种写法无法正常工作
export const client = await createClient(); // 顶层await会导致打包工具出错
于是你最终会使用 getter 模式:
typescript
let client: Client | null = null;

export async function getClient() {
	if (!client) {
		client = await createClient();
	}
	return client;
}

// 所有使用方都必须添加await
const client = await getClient();
每个调用位置都需要
await
,你传递的是Promise而非对象。

The Pattern

解决方案(模式内容)

Make construction synchronous. Attach async work to the object:
typescript
// 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;
Construction returns immediately. The async initialization (loading from disk, connecting to servers) happens in the background and is tracked via
whenSynced
.
让构造过程同步进行,将异步操作附加到对象上:
typescript
// client.ts
export const client = createClient();

// 可立即进行同步访问
client.save(data);
client.load(id);

// 需要时再等待异步操作完成
await client.whenSynced;
构造函数会立即返回。异步初始化操作(从磁盘加载、连接服务器等)在后台执行,并通过
whenSynced
属性跟踪状态。

The UI Render Gate

UI渲染闸门

In Svelte, await once at the root:
svelte
<!-- +layout.svelte -->
<script>
	import { client } from '$lib/client';
</script>

{#await client.whenSynced}
	<LoadingSpinner />
{:then}
	{@render children?.()}
{/await}
The gate guarantees: by the time any child component's script runs, the async work is complete. Children use sync access without checking readiness.
在Svelte中,只需在根节点处等待一次:
svelte
<!-- +layout.svelte -->
<script>
	import { client } from '$lib/client';
</script>

{#await client.whenSynced}
	<LoadingSpinner />
{:then}
	{@render children?.()}
{/await}
这个闸门可以保证:当任何子组件的脚本运行时,异步操作已经完成。子组件可以直接同步访问客户端,无需检查是否就绪。

Implementation

实现方式

The
withCapabilities()
fluent builder attaches async work to a sync-constructed object:
typescript
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),
});
使用
withCapabilities()
流畅构建器将异步操作附加到同步构造的对象上:
typescript
function createClient() {
	const state = initializeSyncState();

	return {
		save(data) {
			/* 同步方法 */
		},
		load(id) {
			/* 同步方法 */
		},

		withCapabilities({ persistence }) {
			const whenSynced = persistence(state);
			return Object.assign(this, { whenSynced });
		},
	};
}

// 使用示例
export const client = createClient().withCapabilities({
	persistence: (state) => loadFromIndexedDB(state),
});

Before and After

前后对比

AspectAsync ConstructionSync + whenSynced
Module exportCan't export directlyExport the object
Consumer code
await getX()
everywhere
Direct import, sync use
UI integrationAwkward promise handlingSingle
{#await}
gate
Type signature
Promise<X>
X
with
.whenSynced
维度异步构造同步构造+whenSynced
模块导出无法直接导出可直接导出对象
消费端代码到处都是
await getX()
直接导入,同步使用
UI集成繁琐的Promise处理单个
{#await}
闸门
类型签名
Promise<X>
.whenSynced
X
类型

Real-World Example: y-indexeddb

真实案例:y-indexeddb

The Yjs ecosystem uses this pattern everywhere:
typescript
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
They never block construction. The async work is always deferred to a property you can await.
Yjs生态系统中广泛使用了此模式:
typescript
const provider = new IndexeddbPersistence('my-db', doc);
// 构造函数立即返回

provider.on('update', handleUpdate); // 可直接同步访问

await provider.whenSynced; // 需要时再等待
他们从不阻塞构造过程,异步操作始终延迟到可等待的属性中。

Related Patterns

相关模式

  • Lazy Singleton — when you need race-condition-safe lazy initialization
  • Don't Use Parallel Maps — attach state to instances instead of tracking separately
  • 懒加载单例 — 当你需要避免竞态条件的懒加载初始化时使用
  • 不要使用并行映射 — 将状态附加到实例上,而非单独跟踪

References

参考资料