# Microservice Framework

一个轻量级的 TypeScript 微服务框架。提供了类型安全、自动客户端生成、请求重试等特性。

## 特性

- 📝 完全的 TypeScript 支持
- 🔄 自动生成类型安全的客户端代码
- 🛡️ 使用 Zod 进行运行时类型验证
- 🔁 内置智能重试机制
- 🎯 支持幂等操作
- 🌟 优雅的装饰器 API
- 🚦 优雅停机支持
- 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用
- 🌟 支持 Stream 流传输，客户端使用 AsyncIterator 迭代
- 🌟 服务引擎支持通过 WebSocket 进行实时通信，相比 HTTP 请求具有以下优势：
  - 保持长连接，减少连接建立的开销
  - 支持双向通信
  - 使用 Brotli 压缩，减少数据传输量
  - 自动重连和心跳检测
- 🌐 内置 PageRenderPlugin 支持服务端渲染页面，集成 HTMX 和 Hyperscript

## TODOs

- [ ] 示例项目
- [ ] 微服务高级功能，熔断器、负载均衡等

## 安装

```typescript
import { Action, Microservice, Module } from "imean-service-engine";
```

## 快速开始

### 1. 定义数据模型

使用 Zod 定义你的数据模型：

```typescript
import { z } from "zod";
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number().min(0).max(150),
});
type User = z.infer<typeof UserSchema>;
```

### 2. 创建服务模块

使用装饰器定义你的服务模块和方法：

```typescript
@Module("users", {
  description: "用户服务模块",
  version: "1.0.0",
})
class UserService {
  private users = new Map<string, User>();

  @Action({
    description: "获取用户信息",
    params: [z.string()],
    returns: UserSchema,
  })
  async getUser(id: string): Promise<User> {
    const user = this.users.get(id);
    if (!user) {
      throw new Error("用户不存在");
    }
    return user;
  }

  @Action({
    description: "创建新用户",
    params: [z.string(), z.number()],
    returns: UserSchema,
  })
  async createUser(name: string, age: number): Promise<User> {
    const id = crypto.randomUUID();
    const user = { id, name, age };
    this.users.set(id, user);
    return user;
  }

  @Action({
    description: "更新用户信息",
    params: [z.string(), z.string(), z.number()],
    returns: UserSchema,
    // 标记为幂等操作，支持自动重试
    idempotence: true,
  })
  async updateUser(id: string, name: string, age: number): Promise<User> {
    const user = this.users.get(id);
    if (!user) {
      throw new Error("用户不存在");
    }
    const updatedUser = { ...user, name, age };
    this.users.set(id, updatedUser);
    return updatedUser;
  }
}
```

### 3. 启动服务

```typescript
const service = new Microservice({
  modules: [UserService],
  prefix: "/api",
});
await service.init();
// 启动在 3000 端口
service.start(3000);
```

### 4. 使用生成的客户端

访问服务根路径（如 `http://localhost:3000/client.ts`）会自动下载生成的
TypeScript 客户端代码。

使用生成的客户端：

```typescript
const client = new MicroserviceClient({
  baseUrl: "http://localhost:3000",
});
// 创建用户
const user = await client.users.createUser("张三", 25);
// 更新用户（支持自动重试）
const updated = await client.users.updateUser(user.id, "张三丰", 30);
// 获取用户
const found = await client.users.getUser(user.id);
```

## 高级特性

### PageRenderPlugin - 服务端渲染页面

PageRenderPlugin 为微服务框架提供了服务端渲染页面的能力，集成了 HTMX 和 Hyperscript，让你可以轻松构建现代化的 Web 应用。

#### 启用 PageRenderPlugin

```typescript
import { Microservice, PageRenderPlugin } from "imean-service-engine";

const service = new Microservice({
  modules: [UserService],
  plugins: [new PageRenderPlugin()],
});
```

#### 使用 @Page 装饰器

使用 `@Page` 装饰器可以将模块方法暴露为 Web 页面：

```typescript
import { Page, HtmxLayout } from "imean-service-engine";

@Module("web")
class WebService {
  @Page({
    path: "/greeting",
    method: "get",
    description: "问候页面",
  })
  greetingPage(ctx: Context) {
    return (
      <HtmxLayout title="问候页面">
        <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
          <div class="max-w-4xl mx-auto">
            <h1 class="text-4xl font-bold text-center text-gray-800 mb-8">
              HTMX 交互示例
            </h1>
            <div class="bg-white rounded-lg shadow-lg p-6">
              <h2 class="text-2xl font-semibold mb-4 text-gray-700">问候语</h2>
              <div id="greeting" class="text-xl p-4 bg-blue-50 rounded-lg">
                欢迎使用微服务框架！
              </div>
              <button
                class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
                hx-post="/api/greeting"
                hx-target="#greeting"
                hx-swap="innerHTML"
              >
                更新问候语
              </button>
            </div>
          </div>
        </div>
      </HtmxLayout>
    );
  }

  @Page({
    path: "/greeting",
    method: "post",
    description: "更新问候语",
  })
  updateGreeting(ctx: Context) {
    return "你好，世界！当前时间：" + new Date().toLocaleString();
  }
}
```

#### JSX 配置

要使用 JSX 语法，需要在 `tsconfig.json` 中配置：

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}
```

#### HtmxLayout 组件

`HtmxLayout` 提供了预配置的页面布局，包含：

- HTMX 库（最新版本）
- Hyperscript 库（最新版本）
- Tailwind CSS（CDN 版本）
- 响应式设计支持
- 默认图标

```typescript
import { HtmxLayout } from "imean-service-engine";

// 基本用法
const page = (
  <HtmxLayout title="我的页面">
    <div>页面内容</div>
  </HtmxLayout>
);

// 自定义图标
const pageWithCustomIcon = (
  <HtmxLayout title="我的页面" favicon={<link rel="icon" href="/custom-icon.ico" />}>
    <div>页面内容</div>
  </HtmxLayout>
);
```

#### BaseLayout 组件

如果你不想使用 HTMX 和 Hyperscript，而是想使用其他前端框架（如 React、Vue 等），可以使用 `BaseLayout` 组件：

```typescript
import { BaseLayout } from "imean-service-engine";

// 使用 BaseLayout 自定义页面
const customPage = (
  <BaseLayout title="自定义页面">
    <div>页面内容</div>
  </BaseLayout>
);

// 自定义头部内容
const pageWithCustomHead = (
  <BaseLayout 
    title="自定义页面"
    heads={
      <>
        <link rel="stylesheet" href="/custom.css" />
        <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
      </>
    }
  >
    <div id="root">React 应用将在这里渲染</div>
  </BaseLayout>
);
```

`BaseLayout` 提供：
- 基本的 HTML 结构
- 可自定义的 `<head>` 内容
- 可自定义的页面标题
- 可自定义的图标

#### HTMX 交互示例

结合 HTMX 可以实现丰富的交互效果：

```typescript
@Page({
  path: "/users",
  method: "get",
  description: "用户列表页面",
})
usersPage(ctx: Context) {
  return (
    <HtmxLayout title="用户管理">
      <div class="container mx-auto p-8">
        <h1 class="text-3xl font-bold mb-6">用户管理</h1>
        
        {/* 用户列表 */}
        <div 
          id="user-list" 
          hx-get="/api/users/list"
          hx-trigger="load"
        >
          加载中...
        </div>
        
        {/* 添加用户表单 */}
        <div class="mt-8 bg-white rounded-lg shadow p-6">
          <h2 class="text-xl font-semibold mb-4">添加新用户</h2>
          <form 
            hx-post="/api/users/add"
            hx-target="#user-list"
            hx-swap="outerHTML"
          >
            <div class="grid grid-cols-2 gap-4">
              <input 
                type="text" 
                name="name" 
                placeholder="姓名"
                class="px-3 py-2 border rounded-md" 
                required 
              />
              <input 
                type="number" 
                name="age" 
                placeholder="年龄"
                class="px-3 py-2 border rounded-md" 
                required 
              />
            </div>
            <button 
              type="submit" 
              class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
            >
              添加用户
            </button>
          </form>
        </div>
      </div>
    </HtmxLayout>
  );
}
```

#### Hyperscript 增强交互

使用 Hyperscript 可以实现更复杂的客户端逻辑：

```typescript
// 带加载状态的按钮
<button
  hx-post="/api/users/refresh"
  hx-target="#user-list"
  hx-swap="innerHTML"
  _="on htmx:beforeRequest hide #button-text then show #loading-spinner end 
     on htmx:afterRequest hide #loading-spinner then show #button-text end"
>
  <span id="loading-spinner" class="htmx-indicator">
    <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
      <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
    </svg>
    加载中...
  </span>
  <span id="button-text">刷新用户列表</span>
</button>
```

#### 服务状态页面

PageRenderPlugin 自动在服务根路径（`/api`）提供服务的状态页面，显示：

- 服务基本信息（名称、版本、环境）
- 模块列表和 API 端点
- 服务健康状态

访问 `http://localhost:3000/api` 即可查看服务状态页面。

#### 最佳实践

1. **页面组织**：将页面逻辑与 API 逻辑分离
2. **组件复用**：使用 HtmxLayout 确保一致的页面结构
3. **渐进增强**：优先使用 HTMX 实现交互，必要时使用 Hyperscript
4. **响应式设计**：利用 Tailwind CSS 构建响应式界面
5. **布局选择**：
   - 使用 `HtmxLayout` 进行快速原型开发和简单交互
   - 使用 `BaseLayout` 集成复杂的前端框架（React、Vue 等）
   - 根据项目需求选择合适的布局组件

```typescript
// 推荐的目录结构
src/
├── pages/           # 页面组件
│   ├── users.tsx
│   └── dashboard.tsx
├── services/        # 服务模块
│   ├── user.ts
│   └── web.ts
└── layouts/         # 自定义布局
    └── admin.tsx
```

### 幂等性和重试机制

框架提供了智能的重试机制，但仅对标记为幂等的操作生效：

重试策略：

- 仅对标记为 `idempotence: true` 的方法进行重试
- 重试间隔：500ms、1000ms、3000ms、5000ms
- 最多重试 4 次

### 优雅停机

在需要停止服务时，可以等待所有重试请求完成：

## API 参考

### 装饰器

#### @Module(name: string, options: ModuleOptions)

定义一个服务模块。

```typescript
interface ModuleOptions {
  description?: string;
  version?: string;
}
```

#### @Action(options: ActionOptions)

定义一个模块方法。

```typescript
interface ActionOptions {
  description?: string;
  params: z.ZodType<any>[]; // 参数类型定义
  returns: z.ZodType<any>; // 返回值类型定义
  idempotence?: boolean; // 是否是幂等操作
  stream?: boolean; // 是否是流式操作
  cache?: boolean; // 是否开启缓存
  cacheTTL?: number; // 缓存过期时间（秒）
}
```

#### @Page(options: PageOptions)

定义一个页面路由（需要启用 PageRenderPlugin）。

```typescript
interface PageOptions {
  method: "get" | "post" | "put" | "delete" | "patch" | "options";
  path: string;
  description?: string;
}
```

示例：

```typescript
@Page({
  path: "/dashboard",
  method: "get",
  description: "仪表板页面",
})
dashboardPage(ctx: Context) {
  return (
    <HtmxLayout title="仪表板">
      <div>仪表板内容</div>
    </HtmxLayout>
  );
}
```

### Microservice

#### constructor(options: MicroserviceOptions)

创建微服务实例。

```typescript
interface MicroserviceOptions {
  modules: (new () => any)[]; // 模块类数组
  prefix?: string; // API 前缀，默认为 "/api"
  plugins?: Plugin[]; // 插件数组，如 PageRenderPlugin
}
```

#### start(port?: number): void

启动服务器，默认端口为 3000。

### MicroserviceClient

#### constructor(options: ClientOptions)

创建客户端实例。

```typescript
interface ClientOptions {
  baseUrl: string; // 服务器地址
  prefix?: string; // API 前缀，默认为 "/api"
  headers?: Record<string, string>; // 自定义请求头
}
```

## 类型安全

框架使用 Zod 进行运行时类型验证，确保：

- 请求参数类型正确
- 返回值类型符合预期
- 自动生成的客户端代码类型完整

## 最佳实践

### 服务启动前检查

框架提供了 `startCheck` 方法用于在服务正式启动前进行必要的检查和初始化。这对于确保依赖服务（如数据库）可用非常有用。

```typescript
// main.ts
import { startCheck } from "imean-service-engine";

// 数据库连接检查
async function checkDatabase() {
  try {
    const db = await connectDB({
      host: "localhost",
      port: 5432,
      // ...其他配置
    });
    await db.ping();
    console.log("✅ 数据库连接成功");
  } catch (error) {
    throw new Error(`数据库连接失败: ${error.message}`);
  }
}

// Redis 连接检查
async function checkRedis() {
  try {
    const redis = await connectRedis();
    await redis.ping();
    console.log("✅ Redis 连接成功");
  } catch (error) {
    throw new Error(`Redis 连接失败: ${error.message}`);
  }
}

// 启动检查
startCheck(
  // 前置检查项
  [checkDatabase, checkRedis],
  // 服务启动回调
  async () => {
    // 使用动态导入载入服务模块
    const { UserService } = await import("./services/user.ts");
    const { OrderService } = await import("./services/order.ts");

    const service = new Microservice({
      modules: [UserService, OrderService],
      prefix: "/api",
    });

    service.start(3000);
  }
);
```

这种方式的优点：

1. **依赖检查**

   - 确保所有必要的外部服务都可用
   - 避免服务启动后才发现依赖问题
   - 提供清晰的错误信息

2. **按需加载**

   - 使用动态导入延迟加载服务模块
   - 避免在检查失败时不必要的资源初始化
   - 提高启动性能

3. **优雅失败**
   - 如果检查失败，服务不会启动
   - 适合在容器环境中使用
   - 便于问题诊断

### 目录结构建议

```
your-service/
├── main.ts              # 入口文件，包含启动检查
├── config/
│   └── index.ts         # 配置文件
├── services/
│   ├── user.ts          # 用户服务模块
│   └── order.ts         # 订单服务模块
├── models/
│   ├── user.ts          # 用户数据模型
│   └── order.ts         # 订单数据模型
├── utils/
│   └── db.ts            # 数据库连接工具
└── tests/
    └── services/
        ├── user.test.ts
        └── order.test.ts
```

### 配置管理

建议将配置和服务逻辑分离：

```typescript
// config/index.ts
export const config = {
  database: {
    host: process.env.DB_HOST || "localhost",
    port: parseInt(process.env.DB_PORT || "5432"),
    // ...
  },
  redis: {
    url: process.env.REDIS_URL || "redis://localhost:6379",
    // ...
  },
  service: {
    port: parseInt(process.env.PORT || "3000"),
    prefix: process.env.API_PREFIX || "/api",
  },
};

// main.ts
import { config } from "./config/index.ts";

startCheck(
  [
    /* ... */
  ],
  async () => {
    const service = new Microservice({
      modules: [
        /* ... */
      ],
      prefix: config.service.prefix,
    });

    service.start(config.service.port);
  }
);
```

### 文件上传/二进制数据

框架传输采用 ejson 进行序列化，支持二进制数据传输。只需要在模型中接受 `Uint8Array` 类型即可，并且 Zod 类型需要设置为 `z.instanceof(Uint8Array)`。

```typescript
import * as z from "zod";

@Module("files")
export class FileService {
  @Action({
    params: [z.instanceof(Uint8Array)],
    returns: z.instanceof(Uint8Array),
  })
  reverseBinary(data: Uint8Array): Uint8Array {
    return data.reverse();
  }
}
```

### 定时任务

框架提供了 `@Schedule` 装饰器用于定义定时任务。在分布式环境中，同一个定时任务只会在一个服务实例上执行。

#### 基本用法

```typescript
@Module("tasks")
class TaskService {
  @Schedule({
    interval: 5000, // 执行间隔（毫秒）
    mode: ScheduleMode.FIXED_RATE, // 执行模式
  })
  async cleanupTask() {
    // 定时执行的任务代码
  }
}
```

#### 执行模式

框架支持两种执行模式：

- `FIXED_RATE`: 固定频率执行，不考虑任务执行时间

  ```typescript
  @Schedule({
    interval: 5000,
    mode: ScheduleMode.FIXED_RATE,
  })
  async quickTask() {
    // 每 5 秒执行一次
  }
  ```

- `FIXED_DELAY`: 固定延迟执行，等待任务完成后再计时
  ```typescript
  @Schedule({
    interval: 5000,
    mode: ScheduleMode.FIXED_DELAY,
  })
  async longRunningTask() {
    // 任务完成后等待 5 秒再执行下一次
  }
  ```

#### 分布式调度

定时任务基于 etcd 实现分布式调度：

1. 自动选主：多个服务实例中只有一个会执行定时任务
2. 故障转移：当执行任务的实例故障时，其他实例会自动接管
3. 服务发现：新加入的实例会自动参与选主

```typescript
const service = new Microservice({
  name: "user-service", // 服务名称
  modules: [TaskService],
  etcd: {
    hosts: ["localhost:2379"], // etcd 服务地址
    auth: {
      // 可选的认证信息
      username: "root",
      password: "password",
    },
    ttl: 10, // 租约 TTL（秒）
    namespace: "services", // 可选的命名空间
  },
});
```

#### 选举 Key (内部工作机制)

每个定时任务都有唯一的选举 key，格式为：

```
{service-name}/{module-name}/schedules/{method-name}
```

#### 优雅停机

服务停止时会自动清理定时任务和选举信息：

```typescript
// 在 k8s 停机信号处理中
await service.stop();
```

#### 注意事项

1. 使用定时任务需要配置 etcd
2. 建议使用 `FIXED_DELAY` 模式执行耗时任务
3. 任务执行时间不应超过执行间隔

#### Stream 流

服务引擎支持 Stream 流传输，可以在服务端返回 Stream 流，客户端使用 `await iter.next()` 逐个获取数据。或者使用 `for await (const item of iter)` 迭代。

> 注意：服务端返回的流需要使用 `AsyncIterableIterator` 类型，客户端使用 `AsyncIterator` 迭代。
> HTTP 请求方式也支持流式传输，服务端是通过 SSE 实现。

服务端：

```typescript
@Module("stream")
class StreamService {
  @Action({
    params: [z.number()],
    returns: z.number,
    stream: true,
  })
  async *stream(count: number): AsyncIterableIterator<number> {
    for (let i = 0; i < count; i++) {
      yield i;
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
  }
}
```

客户端：

```typescript
const client = new MicroserviceClient({
  baseUrl: "http://localhost:3000",
  prefix: "/api",
});

const iter = await client.stream.streamNumbers(10);
for await (const item of iter) {
  console.log(item);
}
```

## WebSocket

服务引擎支持通过 WebSocket 进行实时通信，相比 HTTP 请求具有以下优势：

1. 保持长连接，减少连接建立的开销
2. 支持双向通信
3. 使用 Brotli 压缩，减少数据传输量
4. 自动重连和心跳检测

服务端配置：

```typescript
const service = new Microservice({
  modules: [UserService],
  prefix: "/api",
  websocket: {
    pingInterval: 5000,
  },
});
```

客户端配置：

```typescript
const client = new MicroserviceClient({
  baseUrl: "ws://localhost:3000",
  prefix: "/api",
  websocket: {
    pingInterval: 5000,
  },
});
```

注意：客户端使用 websocket 时，需要安装 brotli-wasm 库。因为服务端使用 brotli 压缩，客户端需要解压。

### Node.js 环境使用 WebSocket

最新Node.js已经提供了 WebSocket 实现，可以直接使用。如果在较低 Node.js 环境下，可以使用 `isomorphic-ws` 包来提供 WebSocket 实现：

```typescript
import WebSocket from "isomorphic-ws";

const client = new MicroserviceClient({
  baseUrl: "http://localhost:3000",
  websocket: {
    WebSocket, // 传入 WebSocket 实现
    timeout: 10000,
    retryInterval: 3000,
    maxRetries: 5,
    pingInterval: 30000,
  },
});

// 使用方法和浏览器环境完全一样
const result = await client.users.getUser("1");
```

安装依赖：

```bash
npm install isomorphic-ws brotli-wasm
```

### 注意事项

1. WebSocket 连接会自动重连，无需手动处理
2. 所有消息都使用 Brotli 压缩，需要安装 brotli-wasm 库
3. 客户端会定期发送心跳消息以保持连接
4. 在不再使用时应调用 `close()` 方法关闭连接
5. Node.js 环境需要安装 `isomorphic-ws` 包
