services-layer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServices Layer Patterns
服务层模式
This skill documents how to implement services in the Whispering architecture. Services are pure, isolated business logic with no UI dependencies that return types for error handling.
Result<T, E>本技能文档介绍了如何在Whispering架构中实现服务。服务是纯业务逻辑,与UI无依赖,返回类型用于错误处理。
Result<T, E>When to Apply This Skill
适用场景
Use this pattern when you need to:
- Create a new service with domain-specific error handling
- Add error types with structured context (like HTTP status codes)
- Understand how services are organized and exported
- Implement platform-specific service variants (desktop vs web)
当你需要以下操作时,可应用此模式:
- 创建带有领域特定错误处理的新服务
- 添加带有结构化上下文(如HTTP状态码)的错误类型
- 理解服务的组织与导出方式
- 实现平台特定的服务变体(桌面端 vs 网页端)
Core Architecture
核心架构
Services follow a three-layer architecture: Service → Query → UI
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ UI │ --> │ RPC/Query │ --> │ Services │
│ Components │ │ Layer │ │ (Pure) │
└─────────────┘ └─────────────┘ └──────────────┘Services are:
- Pure: Accept explicit parameters, no hidden dependencies
- Isolated: No knowledge of UI state, settings, or reactive stores
- Testable: Easy to unit test with mock parameters
- Consistent: All return types for uniform error handling
Result<T, E>
服务遵循三层架构:Service → Query → UI
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ UI │ --> │ RPC/Query │ --> │ Services │
│ Components │ │ Layer │ │ (Pure) │
└─────────────┘ └─────────────┘ └──────────────┘服务具备以下特性:
- 纯函数式:接收明确参数,无隐藏依赖
- 隔离性:不感知UI状态、设置或响应式存储
- 可测试性:通过模拟参数轻松进行单元测试
- 一致性:所有服务均返回类型,实现统一错误处理
Result<T, E>
Creating Tagged Errors with createTaggedError
使用createTaggedError创建标记错误
Every service defines domain-specific errors using from wellcrafted:
createTaggedErrortypescript
import { createTaggedError } from 'wellcrafted/error';
import { Err, Ok, type Result, tryAsync } from 'wellcrafted/result';
// Basic pattern - creates both constructor and Err helper
export const { MyServiceError, MyServiceErr } =
createTaggedError('MyServiceError');
type MyServiceError = ReturnType<typeof MyServiceError>;每个服务都使用wellcrafted中的定义领域特定错误:
createTaggedErrortypescript
import { createTaggedError } from 'wellcrafted/error';
import { Err, Ok, type Result, tryAsync } from 'wellcrafted/result';
// 基础模式 - 同时创建构造函数和Err辅助函数
export const { MyServiceError, MyServiceErr } =
createTaggedError('MyServiceError');
type MyServiceError = ReturnType<typeof MyServiceError>;What createTaggedError Returns
createTaggedError的返回值
createTaggedError('Name')- - Constructor function for creating error objects
NameError - - Helper that wraps the error in
NameErrfor direct returnErr()
typescript
// These are equivalent:
return Err(MyServiceError({ message: 'Something failed' }));
return MyServiceErr({ message: 'Something failed' }); // Shorter formcreateTaggedError('Name')- - 用于创建错误对象的构造函数
NameError - - 将错误包装在
NameErr中的辅助函数,可直接返回Err()
typescript
// 以下两种写法等价:
return Err(MyServiceError({ message: 'Something failed' }));
return MyServiceErr({ message: 'Something failed' }); // 简写形式Adding Typed Context with .withContext()
使用.withContext()添加类型化上下文
For errors that need structured metadata (like HTTP status codes), chain :
.withContext<T>()typescript
type ResponseContext = {
status: number; // HTTP status code
};
export const { ResponseError, ResponseErr } =
createTaggedError('ResponseError').withContext<ResponseContext>();
// Usage: Include context when creating errors
return ResponseErr({
message: 'Request failed',
context: { status: 401 }, // TypeScript enforces this shape
});对于需要结构化元数据(如HTTP状态码)的错误,可链式调用:
.withContext<T>()typescript
type ResponseContext = {
status: number; // HTTP状态码
};
export const { ResponseError, ResponseErr } =
createTaggedError('ResponseError').withContext<ResponseContext>();
// 使用方式:创建错误时包含上下文
return ResponseErr({
message: 'Request failed',
context: { status: 401 }, // TypeScript会强制校验该结构
});Error Type Examples from the Codebase
代码库中的错误类型示例
typescript
// Simple service error (most common)
export const { RecorderServiceError, RecorderServiceErr } = createTaggedError(
'RecorderServiceError',
);
// HTTP errors with status context
export const { ResponseError, ResponseErr } = createTaggedError(
'ResponseError',
).withContext<{ status: number }>();
// Multiple related errors
export const { ConnectionError, ConnectionErr } =
createTaggedError('ConnectionError');
export const { ParseError, ParseErr } = createTaggedError('ParseError');
// Combine into union type
export type HttpServiceError = ConnectionError | ResponseError | ParseError;typescript
// 简单服务错误(最常见)
export const { RecorderServiceError, RecorderServiceErr } = createTaggedError(
'RecorderServiceError',
);
// 带状态上下文的HTTP错误
export const { ResponseError, ResponseErr } = createTaggedError(
'ResponseError',
).withContext<{ status: number }>();
// 多个相关错误
export const { ConnectionError, ConnectionErr } =
createTaggedError('ConnectionError');
export const { ParseError, ParseErr } = createTaggedError('ParseError');
// 合并为联合类型
export type HttpServiceError = ConnectionError | ResponseError | ParseError;Service Implementation Pattern
服务实现模式
Basic Service Structure
基础服务结构
typescript
import { createTaggedError, extractErrorMessage } from 'wellcrafted/error';
import { Err, Ok, type Result, tryAsync, trySync } from 'wellcrafted/result';
// 1. Define domain-specific error type
export const { MyServiceError, MyServiceErr } =
createTaggedError('MyServiceError');
type MyServiceError = ReturnType<typeof MyServiceError>;
// 2. Create factory function that returns service object
export function createMyService() {
return {
async doSomething(options: {
param1: string;
param2: number;
}): Promise<Result<OutputType, MyServiceError>> {
// Input validation
if (!options.param1) {
return MyServiceErr({
message: 'param1 is required',
});
}
// Wrap risky operations with tryAsync
const { data, error } = await tryAsync({
try: () => riskyAsyncOperation(options),
catch: (error) =>
MyServiceErr({
message: `Operation failed: ${extractErrorMessage(error)}`,
}),
});
if (error) return Err(error);
return Ok(data);
},
};
}
// 3. Export the "Live" instance (production singleton)
export type MyService = ReturnType<typeof createMyService>;
export const MyServiceLive = createMyService();typescript
import { createTaggedError, extractErrorMessage } from 'wellcrafted/error';
import { Err, Ok, type Result, tryAsync, trySync } from 'wellcrafted/result';
// 1. 定义领域特定错误类型
export const { MyServiceError, MyServiceErr } =
createTaggedError('MyServiceError');
type MyServiceError = ReturnType<typeof MyServiceError>;
// 2. 创建返回服务对象的工厂函数
export function createMyService() {
return {
async doSomething(options: {
param1: string;
param2: number;
}): Promise<Result<OutputType, MyServiceError>> {
// 输入校验
if (!options.param1) {
return MyServiceErr({
message: 'param1是必填项',
});
}
// 使用tryAsync包装风险操作
const { data, error } = await tryAsync({
try: () => riskyAsyncOperation(options),
catch: (error) =>
MyServiceErr({
message: `操作失败: ${extractErrorMessage(error)}`,
}),
});
if (error) return Err(error);
return Ok(data);
},
};
}
// 3. 导出"Live"实例(生产环境单例)
export type MyService = ReturnType<typeof createMyService>;
export const MyServiceLive = createMyService();Real-World Example: Recorder Service
真实示例:Recorder服务
typescript
// From apps/whispering/src/lib/services/isomorphic/recorder/navigator.ts
export function createNavigatorRecorderService(): RecorderService {
let activeRecording: ActiveRecording | null = null;
return {
getRecorderState: async (): Promise<
Result<WhisperingRecordingState, RecorderServiceError>
> => {
return Ok(activeRecording ? 'RECORDING' : 'IDLE');
},
startRecording: async (
params: NavigatorRecordingParams,
{ sendStatus },
): Promise<Result<DeviceAcquisitionOutcome, RecorderServiceError>> => {
// Validate state
if (activeRecording) {
return RecorderServiceErr({
message:
'A recording is already in progress. Please stop the current recording.',
});
}
// Get stream (calls another service)
const { data: streamResult, error: acquireStreamError } =
await getRecordingStream({ selectedDeviceId, sendStatus });
if (acquireStreamError) {
return RecorderServiceErr({
message: acquireStreamError.message,
});
}
// Initialize MediaRecorder
const { data: mediaRecorder, error: recorderError } = trySync({
try: () =>
new MediaRecorder(stream, {
bitsPerSecond: Number(bitrateKbps) * 1000,
}),
catch: (error) =>
RecorderServiceErr({
message: `Failed to initialize recorder. ${extractErrorMessage(error)}`,
}),
});
if (recorderError) {
cleanupRecordingStream(stream);
return Err(recorderError);
}
// Store state and start
activeRecording = {
recordingId,
stream,
mediaRecorder,
recordedChunks: [],
};
mediaRecorder.start(TIMESLICE_MS);
return Ok(deviceOutcome);
},
};
}
export const NavigatorRecorderServiceLive = createNavigatorRecorderService();typescript
// 来自 apps/whispering/src/lib/services/isomorphic/recorder/navigator.ts
export function createNavigatorRecorderService(): RecorderService {
let activeRecording: ActiveRecording | null = null;
return {
getRecorderState: async (): Promise<
Result<WhisperingRecordingState, RecorderServiceError>
> => {
return Ok(activeRecording ? 'RECORDING' : 'IDLE');
},
startRecording: async (
params: NavigatorRecordingParams,
{ sendStatus },
): Promise<Result<DeviceAcquisitionOutcome, RecorderServiceError>> => {
// 校验状态
if (activeRecording) {
return RecorderServiceErr({
message:
'当前已有录制在进行中,请先停止当前录制。',
});
}
// 获取流(调用其他服务)
const { data: streamResult, error: acquireStreamError } =
await getRecordingStream({ selectedDeviceId, sendStatus });
if (acquireStreamError) {
return RecorderServiceErr({
message: acquireStreamError.message,
});
}
// 初始化MediaRecorder
const { data: mediaRecorder, error: recorderError } = trySync({
try: () =>
new MediaRecorder(stream, {
bitsPerSecond: Number(bitrateKbps) * 1000,
}),
catch: (error) =>
RecorderServiceErr({
message: `初始化录制器失败。${extractErrorMessage(error)}`,
}),
});
if (recorderError) {
cleanupRecordingStream(stream);
return Err(recorderError);
}
// 存储状态并开始录制
activeRecording = {
recordingId,
stream,
mediaRecorder,
recordedChunks: [],
};
mediaRecorder.start(TIMESLICE_MS);
return Ok(deviceOutcome);
},
};
}
export const NavigatorRecorderServiceLive = createNavigatorRecorderService();Namespace Exports Pattern
命名空间导出模式
Services are organized hierarchically and re-exported as namespace objects:
服务采用层级化组织,并作为命名空间对象重新导出:
Folder Structure
文件夹结构
services/
├── desktop/ # Desktop-only (Tauri)
│ ├── index.ts # Re-exports as desktopServices
│ ├── command.ts
│ └── ffmpeg.ts
├── isomorphic/ # Cross-platform
│ ├── index.ts # Re-exports as services
│ ├── transcription/
│ │ ├── index.ts # Re-exports as transcriptions namespace
│ │ ├── cloud/
│ │ │ ├── openai.ts
│ │ │ └── groq.ts
│ │ └── local/
│ │ └── whispercpp.ts
│ └── completion/
│ ├── index.ts
│ └── openai.ts
├── types.ts
└── index.ts # Main entry pointservices/
├── desktop/ # 仅桌面端(Tauri)
│ ├── index.ts # 重新导出为desktopServices
│ ├── command.ts
│ └── ffmpeg.ts
├── isomorphic/ # 跨平台
│ ├── index.ts # 重新导出为services
│ ├── transcription/
│ │ ├── index.ts # 重新导出为transcriptions命名空间
│ │ ├── cloud/
│ │ │ ├── openai.ts
│ │ │ └── groq.ts
│ │ └── local/
│ │ └── whispercpp.ts
│ └── completion/
│ ├── index.ts
│ └── openai.ts
├── types.ts
└── index.ts # 主入口Index File Pattern
索引文件模式
typescript
// services/isomorphic/transcription/index.ts
export { OpenaiTranscriptionServiceLive as openai } from './cloud/openai';
export { GroqTranscriptionServiceLive as groq } from './cloud/groq';
export { WhispercppTranscriptionServiceLive as whispercpp } from './local/whispercpp';
// services/isomorphic/index.ts
import * as transcriptions from './transcription';
import * as completions from './completion';
export const services = {
db: DbServiceLive,
sound: PlaySoundServiceLive,
transcriptions, // Namespace import
completions, // Namespace import
} as const;
// services/index.ts (main entry)
export { services } from './isomorphic';
export { desktopServices } from './desktop';typescript
// services/isomorphic/transcription/index.ts
export { OpenaiTranscriptionServiceLive as openai } from './cloud/openai';
export { GroqTranscriptionServiceLive as groq } from './cloud/groq';
export { WhispercppTranscriptionServiceLive as whispercpp } from './local/whispercpp';
// services/isomorphic/index.ts
import * as transcriptions from './transcription';
import * as completions from './completion';
export const services = {
db: DbServiceLive,
sound: PlaySoundServiceLive,
transcriptions, // 命名空间导入
completions, // 命名空间导入
} as const;
// services/index.ts(主入口)
export { services } from './isomorphic';
export { desktopServices } from './desktop';Consuming Services
服务使用方式
typescript
// In query layer or anywhere
import { services, desktopServices } from '$lib/services';
// Access via namespace
await services.transcriptions.openai.transcribe(blob, options);
await services.transcriptions.groq.transcribe(blob, options);
await services.db.recordings.getAll();
await desktopServices.ffmpeg.compressAudioBlob(blob, options);typescript
// 在查询层或其他任意位置
import { services, desktopServices } from '$lib/services';
// 通过命名空间访问
await services.transcriptions.openai.transcribe(blob, options);
await services.transcriptions.groq.transcribe(blob, options);
await services.db.recordings.getAll();
await desktopServices.ffmpeg.compressAudioBlob(blob, options);Platform-Specific Services
平台特定服务
For services that need different implementations per platform:
针对需要按平台提供不同实现的服务:
Define Shared Interface
定义共享接口
typescript
// services/isomorphic/text/types.ts
export type TextService = {
readFromClipboard(): Promise<Result<string | null, TextServiceError>>;
copyToClipboard(text: string): Promise<Result<void, TextServiceError>>;
writeToCursor(text: string): Promise<Result<void, TextServiceError>>;
};typescript
// services/isomorphic/text/types.ts
export type TextService = {
readFromClipboard(): Promise<Result<string | null, TextServiceError>>;
copyToClipboard(text: string): Promise<Result<void, TextServiceError>>;
writeToCursor(text: string): Promise<Result<void, TextServiceError>>;
};Implement Per Platform
按平台实现
typescript
// services/isomorphic/text/desktop.ts
export function createTextServiceDesktop(): TextService {
return {
copyToClipboard: (text) =>
tryAsync({
try: () => writeText(text), // Tauri API
catch: (error) => TextServiceErr({ message: 'Clipboard write failed' }),
}),
};
}
// services/isomorphic/text/web.ts
export function createTextServiceWeb(): TextService {
return {
copyToClipboard: (text) =>
tryAsync({
try: () => navigator.clipboard.writeText(text), // Browser API
catch: (error) => TextServiceErr({ message: 'Clipboard write failed' }),
}),
};
}typescript
// services/isomorphic/text/desktop.ts
export function createTextServiceDesktop(): TextService {
return {
copyToClipboard: (text) =>
tryAsync({
try: () => writeText(text), // Tauri API
catch: (error) => TextServiceErr({ message: '剪贴板写入失败' }),
}),
};
}
// services/isomorphic/text/web.ts
export function createTextServiceWeb(): TextService {
return {
copyToClipboard: (text) =>
tryAsync({
try: () => navigator.clipboard.writeText(text), // 浏览器API
catch: (error) => TextServiceErr({ message: '剪贴板写入失败' }),
}),
};
}Build-Time Platform Detection
构建时平台检测
typescript
// services/isomorphic/text/index.ts
export const TextServiceLive = window.__TAURI_INTERNALS__
? createTextServiceDesktop()
: createTextServiceWeb();typescript
// services/isomorphic/text/index.ts
export const TextServiceLive = window.__TAURI_INTERNALS__
? createTextServiceDesktop()
: createTextServiceWeb();Error Message Best Practices
错误消息最佳实践
Write error messages that are:
- User-friendly: Explain what happened in plain language
- Actionable: Suggest what the user can do
- Detailed: Include technical details for debugging
typescript
// Good error messages
return RecorderServiceErr({
message:
'Unable to connect to the selected microphone. This could be because the device is already in use by another application, has been disconnected, or lacks proper permissions.',
});
return MyServiceErr({
message: `Failed to parse configuration file. Please check that ${filename} contains valid JSON.`,
});
// Include technical details with extractErrorMessage
return MyServiceErr({
message: `Database operation failed. ${extractErrorMessage(error)}`,
});编写错误消息时应满足:
- 用户友好:用平实语言解释问题
- 可操作:给出用户可执行的建议
- 详细:包含便于调试的技术细节
typescript
// 优秀的错误消息示例
return RecorderServiceErr({
message:
'无法连接到所选麦克风。可能是该设备已被其他应用占用、已断开连接或缺少必要权限。',
});
return MyServiceErr({
message: `解析配置文件失败,请检查${filename}是否包含有效的JSON格式。`,
});
// 使用extractErrorMessage包含技术细节
return MyServiceErr({
message: `数据库操作失败。${extractErrorMessage(error)}`,
});Key Rules
核心规则
- Services never import settings - Pass configuration as parameters
- Services never import UI code - No toasts, no notifications, no WhisperingError
- Always return Result types - Never throw errors
- Use trySync/tryAsync - See the error-handling skill for details
- Export factory + Live instance - Factory for testing, Live for production
- Name errors consistently - pattern
{ServiceName}ServiceError
- 服务从不导入配置 - 配置需作为参数传入
- 服务从不导入UI代码 - 无提示框、无通知、无WhisperingError
- 始终返回Result类型 - 绝不能抛出错误
- 使用trySync/tryAsync - 详情请查看错误处理技能文档
- 导出工厂函数+Live实例 - 工厂函数用于测试,Live实例用于生产环境
- 错误命名保持一致 - 遵循命名模式
{ServiceName}ServiceError
References
参考资料
- See for architecture details
apps/whispering/src/lib/services/README.md - See the skill for how services are consumed
query-layer - See the skill for trySync/tryAsync patterns
error-handling
- 查看了解架构细节
apps/whispering/src/lib/services/README.md - 查看技能文档了解服务的使用方式
query-layer - 查看技能文档了解trySync/tryAsync模式
error-handling