Compare commits

...

15 Commits

Author SHA1 Message Date
github-actions[bot]
0f5aa633d8 chore: release packages (#413)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 18:12:40 +08:00
YHH
85171a0a5c fix(database): include dist directory in npm package (#412)
* fix(database): include dist directory in npm package

* fix(ci): add database packages to release build
2025-12-31 18:10:40 +08:00
github-actions[bot]
35d81880a7 chore: release packages (#411)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 16:33:05 +08:00
YHH
71022abc99 feat(database): add database layer architecture (#410)
- Add @esengine/database-drivers for MongoDB/Redis connection management
- Add @esengine/database for Repository pattern with CRUD, pagination, soft delete
- Refactor @esengine/transaction MongoStorage to use shared connection
- Add comprehensive documentation in Chinese and English
2025-12-31 16:26:53 +08:00
github-actions[bot]
87f71e2251 chore: release packages (#409)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 14:32:18 +08:00
YHH
b9ea8d14cf feat(behavior-tree): add action() and condition() methods to BehaviorTreeBuilder (#408)
- Add action(implementationType, name?, config?) for custom action executors
- Add condition(implementationType, name?, config?) for custom condition executors
- Update documentation (EN and CN) with usage examples
- Add test script to package.json
2025-12-31 14:30:31 +08:00
yhh
10d0fb1d5c fix(rapier2d): fix external config path mismatch in tsup 2025-12-31 13:25:30 +08:00
github-actions[bot]
71e111415f chore: release packages (#407)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 12:18:18 +08:00
YHH
0de45279e6 fix(behavior-tree): export NodeExecutorMetadata as value instead of type (#406) 2025-12-31 12:16:17 +08:00
github-actions[bot]
cc6f12d470 chore: release packages (#405)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 10:11:24 +08:00
YHH
902c0a1074 chore: add changeset for HTTP file routing (#404) 2025-12-31 10:06:40 +08:00
yhh
d3e489aad3 feat(server): add HTTP file-based routing support
- Add file-based HTTP routing with httpDir and httpPrefix config options
- Create defineHttp<TBody>() helper for type-safe route definitions
- Support dynamic routes with [param].ts file naming convention
- Add CORS support for cross-origin requests
- Allow merging file routes with inline http config
- RPC server now supports attaching to existing HTTP server via server option
- Add comprehensive documentation for HTTP routing
2025-12-31 09:53:12 +08:00
yhh
12051d987f docs(network): add custom authentication provider documentation
- Add IAuthProvider interface documentation
- Add database password authentication example
- Add OAuth/third-party authentication example
- Add API Key authentication example
- Add guide for using and combining multiple providers
2025-12-30 22:46:40 +08:00
yhh
b38fe5ebf4 docs(editor): improve editor-app build documentation and add build:rapier2d script
- Add `pnpm build:rapier2d` command to automate Rapier2D WASM build process
- Fix gen-src.mjs path to correctly locate thirdparty/rapier.js
- Update init.ts to work with new wasm-pack web target (auto-initialization)
- Fix behavior-tree-editor build config for asset-system dependency
- Update README_CN.md and README.md with simplified build instructions
2025-12-30 22:33:06 +08:00
yhh
f01ce1e320 chore: update lawn-mower-demo submodule (airstrike sync fix) 2025-12-30 21:21:51 +08:00
79 changed files with 7983 additions and 247 deletions

View File

@@ -57,6 +57,9 @@ jobs:
pnpm --filter "@esengine/rpc" build
pnpm --filter "@esengine/network" build
pnpm --filter "@esengine/server" build
pnpm --filter "@esengine/database-drivers" build
pnpm --filter "@esengine/database" build
pnpm --filter "@esengine/transaction" build
pnpm --filter "@esengine/cli" build
pnpm --filter "create-esengine-server" build

View File

@@ -182,6 +182,70 @@ export class IsHealthLow implements INodeExecutor {
}
```
## Using Custom Executors in BehaviorTreeBuilder
After defining a custom executor with `@NodeExecutorMetadata`, use the `.action()` method in the builder:
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// Use custom executor in behavior tree
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// Use custom action - matches implementationType in decorator
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToTarget', 'Chase')
.end()
.action('WaitAction', 'Idle', { duration: 1000 })
.end()
.build();
// Start the behavior tree
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### Builder Methods for Custom Nodes
| Method | Description |
|--------|-------------|
| `.action(type, name?, config?)` | Add custom action node |
| `.condition(type, name?, config?)` | Add custom condition node |
| `.executeAction(name)` | Use blackboard function `action_{name}` |
| `.executeCondition(name)` | Use blackboard function `condition_{name}` |
### Complete Example
```typescript
// 1. Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`Attacking with ${damage} damage!`);
return TaskStatus.Success;
}
}
// 2. Build and use
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## Registering Custom Executors
Executors are auto-registered via the decorator. To manually register:

View File

@@ -0,0 +1,136 @@
---
title: "Database Drivers"
description: "MongoDB, Redis connection management and driver abstraction"
---
`@esengine/database-drivers` is ESEngine's database connection management layer, providing unified connection management for MongoDB, Redis, and more.
## Features
- **Connection Pool** - Automatic connection pool management
- **Auto Reconnect** - Automatic reconnection on disconnect
- **Event Notification** - Connection state change events
- **Type Decoupling** - Simplified interfaces, no dependency on native driver types
- **Shared Connections** - Single connection shared across modules
## Installation
```bash
npm install @esengine/database-drivers
```
**Peer Dependencies:**
```bash
npm install mongodb # For MongoDB support
npm install ioredis # For Redis support
```
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ @esengine/database-drivers (Layer 1) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MongoConnection │ │ RedisConnection │ │
│ │ - Pool management │ │ - Auto-reconnect │ │
│ │ - Auto-reconnect │ │ - Key prefix │ │
│ │ - Event emitter │ │ - Event emitter │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IMongoCollection<T> │ ← Type-safe interface │
│ │ (Adapter pattern) │ decoupled from mongodb types │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────┐ ┌───────────────────────┐
│ @esengine/database │ │ @esengine/transaction │
│ (Repository pattern) │ │ (Distributed tx) │
└───────────────────────┘ └───────────────────────┘
```
## Quick Start
### MongoDB Connection
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
// Create connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20
},
autoReconnect: true
})
// Listen to events
mongo.on('connected', () => console.log('MongoDB connected'))
mongo.on('disconnected', () => console.log('MongoDB disconnected'))
mongo.on('error', (e) => console.error('Error:', e.error))
// Connect
await mongo.connect()
// Use collections
const users = mongo.collection<User>('users')
await users.insertOne({ name: 'John', score: 100 })
const user = await users.findOne({ name: 'John' })
// Disconnect when done
await mongo.disconnect()
```
### Redis Connection
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'game:',
autoReconnect: true
})
await redis.connect()
// Basic operations
await redis.set('session:123', 'data', 3600) // With TTL
const value = await redis.get('session:123')
await redis.disconnect()
```
## Service Container Integration
```typescript
import { ServiceContainer } from '@esengine/ecs-framework'
import {
createMongoConnection,
MongoConnectionToken,
RedisConnectionToken
} from '@esengine/database-drivers'
const services = new ServiceContainer()
// Register connections
const mongo = createMongoConnection({ uri: '...', database: 'game' })
await mongo.connect()
services.register(MongoConnectionToken, mongo)
// Retrieve in other modules
const connection = services.get(MongoConnectionToken)
const users = connection.collection('users')
```
## Documentation
- [MongoDB Connection](/en/modules/database-drivers/mongo/) - MongoDB configuration details
- [Redis Connection](/en/modules/database-drivers/redis/) - Redis configuration details
- [Service Tokens](/en/modules/database-drivers/tokens/) - Dependency injection integration

View File

@@ -0,0 +1,217 @@
---
title: "Database Repository"
description: "Repository pattern database layer with CRUD, pagination, and soft delete"
---
`@esengine/database` is ESEngine's database operation layer, providing type-safe CRUD operations based on the Repository pattern.
## Features
- **Repository Pattern** - Generic CRUD operations with type safety
- **Pagination** - Built-in pagination support
- **Soft Delete** - Optional soft delete with restore
- **User Management** - Ready-to-use UserRepository
- **Password Security** - Secure password hashing with scrypt
## Installation
```bash
npm install @esengine/database @esengine/database-drivers
```
## Quick Start
### Basic Repository
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { Repository, createRepository } from '@esengine/database'
// Define entity
interface Player {
id: string
name: string
score: number
createdAt: Date
updatedAt: Date
}
// Create connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// Create repository
const playerRepo = createRepository<Player>(mongo, 'players')
// CRUD operations
const player = await playerRepo.create({
name: 'John',
score: 0
})
const found = await playerRepo.findById(player.id)
await playerRepo.update(player.id, { score: 100 })
await playerRepo.delete(player.id)
```
### Custom Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
import type { IMongoConnection } from '@esengine/database-drivers'
interface Player extends BaseEntity {
name: string
score: number
rank?: string
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
return this.findMany({
where: { rank }
})
}
}
// Usage
const playerRepo = new PlayerRepository(mongo)
const topPlayers = await playerRepo.findTopPlayers(5)
```
### User Repository
```typescript
import { UserRepository } from '@esengine/database'
const userRepo = new UserRepository(mongo)
// Register new user
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com'
})
// Authenticate
const authenticated = await userRepo.authenticate('john', 'securePassword123')
if (authenticated) {
console.log('Login successful:', authenticated.username)
}
// Change password
await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456')
// Role management
await userRepo.addRole(user.id, 'admin')
await userRepo.removeRole(user.id, 'admin')
// Find users
const admins = await userRepo.findByRole('admin')
const john = await userRepo.findByUsername('john')
```
### Pagination
```typescript
const result = await playerRepo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { rank: 'gold' },
sort: { score: 'desc' }
}
)
console.log(result.data) // Player[]
console.log(result.total) // Total count
console.log(result.totalPages) // Total pages
console.log(result.hasNext) // Has next page
console.log(result.hasPrev) // Has previous page
```
### Soft Delete
```typescript
// Enable soft delete
const playerRepo = createRepository<Player>(mongo, 'players', true)
// Delete (marks as deleted)
await playerRepo.delete(playerId)
// Find excludes soft-deleted by default
const players = await playerRepo.findMany()
// Include soft-deleted records
const allPlayers = await playerRepo.findMany({
includeSoftDeleted: true
})
// Restore soft-deleted record
await playerRepo.restore(playerId)
```
### Query Options
```typescript
// Complex queries
const players = await playerRepo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] },
name: { $like: 'John%' }
},
sort: {
score: 'desc',
name: 'asc'
},
limit: 10,
offset: 0
})
// OR conditions
const players = await playerRepo.findMany({
where: {
$or: [
{ score: { $gte: 1000 } },
{ rank: 'legendary' }
]
}
})
```
## Query Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `$eq` | Equal | `{ score: { $eq: 100 } }` |
| `$ne` | Not equal | `{ status: { $ne: 'banned' } }` |
| `$gt` | Greater than | `{ score: { $gt: 50 } }` |
| `$gte` | Greater or equal | `{ level: { $gte: 10 } }` |
| `$lt` | Less than | `{ age: { $lt: 18 } }` |
| `$lte` | Less or equal | `{ price: { $lte: 100 } }` |
| `$in` | In array | `{ rank: { $in: ['gold', 'platinum'] } }` |
| `$nin` | Not in array | `{ status: { $nin: ['banned'] } }` |
| `$like` | Pattern match | `{ name: { $like: '%john%' } }` |
| `$regex` | Regex match | `{ email: { $regex: '@gmail.com$' } }` |
## Documentation
- [Repository API](/en/modules/database/repository/) - Repository detailed API
- [User Management](/en/modules/database/user/) - UserRepository usage
- [Query Syntax](/en/modules/database/query/) - Query condition syntax

View File

@@ -36,6 +36,13 @@ ESEngine provides a rich set of modules that can be imported as needed.
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
| [Transaction](/en/modules/transaction/) | `@esengine/transaction` | Game transactions with distributed support |
### Database
| Module | Package | Description |
|--------|---------|-------------|
| [Database Drivers](/en/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB, Redis connection management |
| [Database Repository](/en/modules/database/) | `@esengine/database` | Repository pattern data operations |
## Installation
All modules can be installed independently:

View File

@@ -92,6 +92,355 @@ const token = jwtProvider.sign({
const payload = jwtProvider.decode(token)
```
### Custom Provider
You can create custom authentication providers by implementing the `IAuthProvider` interface to integrate with any authentication system (OAuth, LDAP, custom database auth, etc.).
#### IAuthProvider Interface
```typescript
interface IAuthProvider<TUser = unknown, TCredentials = unknown> {
/** Provider name */
readonly name: string;
/** Verify credentials */
verify(credentials: TCredentials): Promise<AuthResult<TUser>>;
/** Refresh token (optional) */
refresh?(token: string): Promise<AuthResult<TUser>>;
/** Revoke token (optional) */
revoke?(token: string): Promise<boolean>;
}
interface AuthResult<TUser> {
success: boolean;
user?: TUser;
error?: string;
errorCode?: AuthErrorCode;
token?: string;
expiresAt?: number;
}
type AuthErrorCode =
| 'INVALID_CREDENTIALS'
| 'EXPIRED_TOKEN'
| 'INVALID_TOKEN'
| 'USER_NOT_FOUND'
| 'ACCOUNT_DISABLED'
| 'RATE_LIMITED'
| 'INSUFFICIENT_PERMISSIONS';
```
#### Custom Provider Examples
**Example 1: Database Password Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface User {
id: string
username: string
roles: string[]
}
interface PasswordCredentials {
username: string
password: string
}
class DatabaseAuthProvider implements IAuthProvider<User, PasswordCredentials> {
readonly name = 'database'
async verify(credentials: PasswordCredentials): Promise<AuthResult<User>> {
const { username, password } = credentials
// Query user from database
const user = await db.users.findByUsername(username)
if (!user) {
return {
success: false,
error: 'User not found',
errorCode: 'USER_NOT_FOUND'
}
}
// Verify password (using bcrypt or similar)
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
return {
success: false,
error: 'Invalid password',
errorCode: 'INVALID_CREDENTIALS'
}
}
// Check account status
if (user.disabled) {
return {
success: false,
error: 'Account is disabled',
errorCode: 'ACCOUNT_DISABLED'
}
}
return {
success: true,
user: {
id: user.id,
username: user.username,
roles: user.roles
}
}
}
}
```
**Example 2: OAuth/Third-party Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface OAuthUser {
id: string
email: string
name: string
provider: string
roles: string[]
}
interface OAuthCredentials {
provider: 'google' | 'github' | 'discord'
accessToken: string
}
class OAuthProvider implements IAuthProvider<OAuthUser, OAuthCredentials> {
readonly name = 'oauth'
async verify(credentials: OAuthCredentials): Promise<AuthResult<OAuthUser>> {
const { provider, accessToken } = credentials
try {
// Verify token with provider
const profile = await this.fetchUserProfile(provider, accessToken)
// Find or create local user
let user = await db.users.findByOAuth(provider, profile.id)
if (!user) {
user = await db.users.create({
oauthProvider: provider,
oauthId: profile.id,
email: profile.email,
name: profile.name,
roles: ['player']
})
}
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
provider,
roles: user.roles
}
}
} catch (error) {
return {
success: false,
error: 'OAuth verification failed',
errorCode: 'INVALID_TOKEN'
}
}
}
private async fetchUserProfile(provider: string, token: string) {
switch (provider) {
case 'google':
return fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
case 'github':
return fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
// Other providers...
default:
throw new Error(`Unsupported provider: ${provider}`)
}
}
}
```
**Example 3: API Key Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface ApiUser {
id: string
name: string
roles: string[]
rateLimit: number
}
class ApiKeyAuthProvider implements IAuthProvider<ApiUser, string> {
readonly name = 'api-key'
private revokedKeys = new Set<string>()
async verify(apiKey: string): Promise<AuthResult<ApiUser>> {
if (!apiKey || !apiKey.startsWith('sk_')) {
return {
success: false,
error: 'Invalid API Key format',
errorCode: 'INVALID_TOKEN'
}
}
if (this.revokedKeys.has(apiKey)) {
return {
success: false,
error: 'API Key has been revoked',
errorCode: 'INVALID_TOKEN'
}
}
// Query API Key from database
const keyData = await db.apiKeys.findByKey(apiKey)
if (!keyData) {
return {
success: false,
error: 'API Key not found',
errorCode: 'INVALID_CREDENTIALS'
}
}
// Check expiration
if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
return {
success: false,
error: 'API Key has expired',
errorCode: 'EXPIRED_TOKEN'
}
}
return {
success: true,
user: {
id: keyData.userId,
name: keyData.name,
roles: keyData.roles,
rateLimit: keyData.rateLimit
},
expiresAt: keyData.expiresAt
}
}
async revoke(apiKey: string): Promise<boolean> {
this.revokedKeys.add(apiKey)
await db.apiKeys.revoke(apiKey)
return true
}
}
```
#### Using Custom Providers
```typescript
import { createServer } from '@esengine/server'
import { withAuth } from '@esengine/server/auth'
// Create custom provider
const dbAuthProvider = new DatabaseAuthProvider()
// Or use OAuth provider
const oauthProvider = new OAuthProvider()
// Use custom provider
const server = withAuth(await createServer({ port: 3000 }), {
provider: dbAuthProvider, // or oauthProvider
// Extract credentials from WebSocket connection request
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
// For database auth: get from query params
const username = url.searchParams.get('username')
const password = url.searchParams.get('password')
if (username && password) {
return { username, password }
}
// For OAuth: get from token param
const provider = url.searchParams.get('provider')
const accessToken = url.searchParams.get('access_token')
if (provider && accessToken) {
return { provider, accessToken }
}
// For API Key: get from header
const apiKey = req.headers['x-api-key']
if (apiKey) {
return apiKey as string
}
return null
},
onAuthFailure: (conn, error) => {
console.log(`Auth failed: ${error.errorCode} - ${error.error}`)
}
})
await server.start()
```
#### Combining Multiple Providers
You can create a composite provider to support multiple authentication methods:
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface MultiAuthCredentials {
type: 'jwt' | 'oauth' | 'apikey' | 'password'
data: unknown
}
class MultiAuthProvider implements IAuthProvider<User, MultiAuthCredentials> {
readonly name = 'multi'
constructor(
private jwtProvider: JwtAuthProvider<User>,
private oauthProvider: OAuthProvider,
private apiKeyProvider: ApiKeyAuthProvider,
private dbProvider: DatabaseAuthProvider
) {}
async verify(credentials: MultiAuthCredentials): Promise<AuthResult<User>> {
switch (credentials.type) {
case 'jwt':
return this.jwtProvider.verify(credentials.data as string)
case 'oauth':
return this.oauthProvider.verify(credentials.data as OAuthCredentials)
case 'apikey':
return this.apiKeyProvider.verify(credentials.data as string)
case 'password':
return this.dbProvider.verify(credentials.data as PasswordCredentials)
default:
return {
success: false,
error: 'Unsupported authentication type',
errorCode: 'INVALID_CREDENTIALS'
}
}
}
}
```
### Session Provider
Use server-side sessions for stateful authentication:

View File

@@ -79,10 +79,140 @@ await server.start()
| `tickRate` | `number` | `20` | Global tick rate (Hz) |
| `apiDir` | `string` | `'src/api'` | API handlers directory |
| `msgDir` | `string` | `'src/msg'` | Message handlers directory |
| `httpDir` | `string` | `'src/http'` | HTTP routes directory |
| `httpPrefix` | `string` | `'/api'` | HTTP routes prefix |
| `cors` | `boolean \| CorsOptions` | - | CORS configuration |
| `onStart` | `(port) => void` | - | Start callback |
| `onConnect` | `(conn) => void` | - | Connection callback |
| `onDisconnect` | `(conn) => void` | - | Disconnect callback |
## HTTP Routing
Supports HTTP API sharing the same port with WebSocket, ideal for login, registration, and similar scenarios.
### File-based Routing
Create route files in the `httpDir` directory, automatically mapped to HTTP endpoints:
```
src/http/
├── login.ts → POST /api/login
├── register.ts → POST /api/register
├── health.ts → GET /api/health (set method: 'GET')
└── users/
└── [id].ts → POST /api/users/:id (dynamic route)
```
### Define Routes
Use `defineHttp` to define type-safe route handlers:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server'
interface LoginBody {
username: string
password: string
}
export default defineHttp<LoginBody>({
method: 'POST', // Default POST, options: GET/PUT/DELETE/PATCH
handler(req, res) {
const { username, password } = req.body
// Validate credentials...
if (!isValid(username, password)) {
res.error(401, 'Invalid credentials')
return
}
// Generate token...
res.json({ token: '...', userId: '...' })
}
})
```
### Request Object (HttpRequest)
```typescript
interface HttpRequest {
raw: IncomingMessage // Node.js raw request
method: string // Request method
path: string // Request path
query: Record<string, string> // Query parameters
headers: Record<string, string | string[] | undefined>
body: unknown // Parsed JSON body
ip: string // Client IP
}
```
### Response Object (HttpResponse)
```typescript
interface HttpResponse {
raw: ServerResponse // Node.js raw response
status(code: number): HttpResponse // Set status code (chainable)
header(name: string, value: string): HttpResponse // Set header (chainable)
json(data: unknown): void // Send JSON
text(data: string): void // Send text
error(code: number, message: string): void // Send error
}
```
### Usage Example
```typescript
// Complete login server example
import { createServer, defineHttp } from '@esengine/server'
import { createJwtAuthProvider, withAuth } from '@esengine/server/auth'
const jwtProvider = createJwtAuthProvider({
secret: process.env.JWT_SECRET!,
expiresIn: 3600 * 24,
})
const server = await createServer({
port: 8080,
httpDir: 'src/http',
httpPrefix: '/api',
cors: true,
})
// Wrap with auth (WebSocket connections validate token)
const authServer = withAuth(server, {
provider: jwtProvider,
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
return url.searchParams.get('token')
},
})
await authServer.start()
// HTTP: http://localhost:8080/api/*
// WebSocket: ws://localhost:8080?token=xxx
```
### Inline Routes
Routes can also be defined directly in configuration (merged with file routes, inline takes priority):
```typescript
const server = await createServer({
port: 8080,
http: {
'/health': {
GET: (req, res) => res.json({ status: 'ok' }),
},
'/webhook': async (req, res) => {
// Accepts all methods
await handleWebhook(req.body)
res.json({ received: true })
},
},
})
```
## Room System
Room is the base class for game rooms, managing players and game state.

View File

@@ -125,23 +125,24 @@ tx:data:{key} - Business data
## MongoStorage
MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses factory pattern with lazy connection.
MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses shared connection from `@esengine/database-drivers`.
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
import { createMongoConnection } from '@esengine/database-drivers';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// Factory pattern: lazy connection, connects on first operation
const storage = new MongoStorage({
factory: async () => {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
return client;
},
database: 'game',
transactionCollection: 'transactions', // Transaction log collection
dataCollection: 'transaction_data', // Business data collection
lockCollection: 'transaction_locks', // Lock collection
// Create shared connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// Create storage using shared connection
const storage = createMongoStorage(mongo, {
transactionCollection: 'transactions', // Transaction log collection (optional)
dataCollection: 'transaction_data', // Business data collection (optional)
lockCollection: 'transaction_locks', // Lock collection (optional)
});
// Create indexes (run on first startup)
@@ -149,11 +150,14 @@ await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
// Close connection when done
// Close storage (does not close shared connection)
await storage.close();
// Or use await using for automatic cleanup (TypeScript 5.2+)
await using storage = new MongoStorage({ ... });
// Shared connection can continue to be used by other modules
const userRepo = new UserRepository(mongo); // @esengine/database
// Finally close the shared connection
await mongo.disconnect();
```
### Characteristics

View File

@@ -606,6 +606,107 @@ export class RetryDecorator implements INodeExecutor {
}
```
## 在代码中使用自定义执行器
定义了自定义执行器后,可以通过 `BehaviorTreeBuilder``.action()``.condition()` 方法在代码中使用:
### 使用 action() 方法
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// 使用自定义执行器构建行为树
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// 使用自定义动作 - implementationType 匹配装饰器中的定义
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToPosition', 'Chase', { speed: 10 })
.end()
.action('DelayAction', 'Idle', { duration: 1.0 })
.end()
.build();
// 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### 使用 condition() 方法
```typescript
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.sequence('AttackBranch')
// 使用自定义条件
.condition('CheckHealth', 'IsHealthy', { threshold: 50, operator: 'greater' })
.action('AttackAction', 'Attack')
.end()
.end()
.build();
```
### Builder 方法对照表
| 方法 | 说明 | 使用场景 |
|------|------|----------|
| `.action(type, name?, config?)` | 使用自定义动作执行器 | 自定义 Action 类 |
| `.condition(type, name?, config?)` | 使用自定义条件执行器 | 自定义 Condition 类 |
| `.executeAction(name)` | 调用黑板函数 `action_{name}` | 简单逻辑、快速原型 |
| `.executeCondition(name)` | 调用黑板函数 `condition_{name}` | 简单条件判断 |
### 完整示例
```typescript
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
NodeExecutorMetadata,
INodeExecutor,
NodeExecutionContext,
TaskStatus,
NodeType,
BindingHelper
} from '@esengine/behavior-tree';
// 1. 定义自定义执行器
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: '攻击',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`执行攻击,造成 ${damage} 点伤害!`);
return TaskStatus.Success;
}
}
// 2. 构建行为树
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('MainBehavior')
.sequence('AttackBranch')
.condition('CheckHealth', 'HasEnoughHealth', { threshold: 20, operator: 'greater' })
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.log('逃跑', 'Flee')
.end()
.build();
// 3. 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, enemyAI);
```
## 注册执行器
### 自动注册

View File

@@ -0,0 +1,136 @@
---
title: "数据库驱动"
description: "MongoDB、Redis 等数据库的连接管理和驱动封装"
---
`@esengine/database-drivers` 是 ESEngine 的数据库连接管理层,提供 MongoDB、Redis 等数据库的统一连接管理。
## 特性
- **连接池管理** - 自动管理连接池,优化资源使用
- **自动重连** - 连接断开时自动重连
- **事件通知** - 连接状态变化事件
- **类型解耦** - 简化接口,不依赖原生驱动类型
- **共享连接** - 单一连接可供多个模块共享
## 安装
```bash
npm install @esengine/database-drivers
```
**对等依赖:**
```bash
npm install mongodb # MongoDB 支持
npm install ioredis # Redis 支持
```
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ @esengine/database-drivers (Layer 1) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MongoConnection │ │ RedisConnection │ │
│ │ - 连接池管理 │ │ - 自动重连 │ │
│ │ - 自动重连 │ │ - Key 前缀 │ │
│ │ - 事件发射器 │ │ - 事件发射器 │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IMongoCollection<T> │ ← 类型安全接口 │
│ │ (适配器模式) │ 与 mongodb 类型解耦 │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────┐ ┌───────────────────────┐
│ @esengine/database │ │ @esengine/transaction │
│ (仓库模式) │ │ (分布式事务) │
└───────────────────────┘ └───────────────────────┘
```
## 快速开始
### MongoDB 连接
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
// 创建连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20
},
autoReconnect: true
})
// 监听事件
mongo.on('connected', () => console.log('MongoDB 已连接'))
mongo.on('disconnected', () => console.log('MongoDB 已断开'))
mongo.on('error', (e) => console.error('错误:', e.error))
// 建立连接
await mongo.connect()
// 使用集合
const users = mongo.collection<User>('users')
await users.insertOne({ name: 'John', score: 100 })
const user = await users.findOne({ name: 'John' })
// 完成后断开连接
await mongo.disconnect()
```
### Redis 连接
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'game:',
autoReconnect: true
})
await redis.connect()
// 基本操作
await redis.set('session:123', 'data', 3600) // 带 TTL
const value = await redis.get('session:123')
await redis.disconnect()
```
## 服务容器集成
```typescript
import { ServiceContainer } from '@esengine/ecs-framework'
import {
createMongoConnection,
MongoConnectionToken,
RedisConnectionToken
} from '@esengine/database-drivers'
const services = new ServiceContainer()
// 注册连接
const mongo = createMongoConnection({ uri: '...', database: 'game' })
await mongo.connect()
services.register(MongoConnectionToken, mongo)
// 在其他模块中获取
const connection = services.get(MongoConnectionToken)
const users = connection.collection('users')
```
## 文档
- [MongoDB 连接](/modules/database-drivers/mongo/) - MongoDB 连接详细配置
- [Redis 连接](/modules/database-drivers/redis/) - Redis 连接详细配置
- [服务令牌](/modules/database-drivers/tokens/) - 依赖注入集成

View File

@@ -0,0 +1,265 @@
---
title: "MongoDB 连接"
description: "MongoDB 连接管理、连接池、自动重连"
---
## 配置选项
```typescript
interface MongoConnectionConfig {
/** MongoDB 连接 URI */
uri: string
/** 数据库名称 */
database: string
/** 连接池配置 */
pool?: {
minSize?: number // 最小连接数
maxSize?: number // 最大连接数
acquireTimeout?: number // 获取连接超时(毫秒)
maxLifetime?: number // 连接最大生命周期(毫秒)
}
/** 是否自动重连(默认 true */
autoReconnect?: boolean
/** 重连间隔(毫秒,默认 5000 */
reconnectInterval?: number
/** 最大重连次数(默认 10 */
maxReconnectAttempts?: number
}
```
## 完整示例
```typescript
import { createMongoConnection, MongoConnectionToken } from '@esengine/database-drivers'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20,
acquireTimeout: 5000,
maxLifetime: 300000
},
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// 事件监听
mongo.on('connected', () => {
console.log('MongoDB 已连接')
})
mongo.on('disconnected', () => {
console.log('MongoDB 已断开')
})
mongo.on('reconnecting', () => {
console.log('MongoDB 正在重连...')
})
mongo.on('reconnected', () => {
console.log('MongoDB 重连成功')
})
mongo.on('error', (event) => {
console.error('MongoDB 错误:', event.error)
})
// 连接
await mongo.connect()
// 检查状态
console.log('已连接:', mongo.isConnected())
console.log('Ping:', await mongo.ping())
```
## IMongoConnection 接口
```typescript
interface IMongoConnection {
/** 连接 ID */
readonly id: string
/** 连接状态 */
readonly state: ConnectionState
/** 建立连接 */
connect(): Promise<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取类型化集合 */
collection<T extends object>(name: string): IMongoCollection<T>
/** 获取数据库接口 */
getDatabase(): IMongoDatabase
/** 获取原生客户端(高级用法) */
getNativeClient(): MongoClientType
/** 获取原生数据库(高级用法) */
getNativeDatabase(): Db
}
```
## IMongoCollection 接口
类型安全的集合接口,与原生 MongoDB 类型解耦:
```typescript
interface IMongoCollection<T extends object> {
readonly name: string
// 查询
findOne(filter: object, options?: FindOptions): Promise<T | null>
find(filter: object, options?: FindOptions): Promise<T[]>
countDocuments(filter?: object): Promise<number>
// 插入
insertOne(doc: T): Promise<InsertOneResult>
insertMany(docs: T[]): Promise<InsertManyResult>
// 更新
updateOne(filter: object, update: object): Promise<UpdateResult>
updateMany(filter: object, update: object): Promise<UpdateResult>
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// 删除
deleteOne(filter: object): Promise<DeleteResult>
deleteMany(filter: object): Promise<DeleteResult>
// 索引
createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string>
}
```
## 使用示例
### 基本 CRUD
```typescript
interface User {
id: string
name: string
email: string
score: number
}
const users = mongo.collection<User>('users')
// 插入
await users.insertOne({
id: '1',
name: 'John',
email: 'john@example.com',
score: 100
})
// 查询
const user = await users.findOne({ name: 'John' })
const topUsers = await users.find(
{ score: { $gte: 100 } },
{ sort: { score: -1 }, limit: 10 }
)
// 更新
await users.updateOne(
{ id: '1' },
{ $inc: { score: 10 } }
)
// 删除
await users.deleteOne({ id: '1' })
```
### 批量操作
```typescript
// 批量插入
await users.insertMany([
{ id: '1', name: 'Alice', email: 'alice@example.com', score: 100 },
{ id: '2', name: 'Bob', email: 'bob@example.com', score: 200 },
{ id: '3', name: 'Carol', email: 'carol@example.com', score: 150 }
])
// 批量更新
await users.updateMany(
{ score: { $lt: 100 } },
{ $set: { status: 'inactive' } }
)
// 批量删除
await users.deleteMany({ status: 'inactive' })
```
### 索引管理
```typescript
// 创建索引
await users.createIndex({ email: 1 }, { unique: true })
await users.createIndex({ score: -1 })
await users.createIndex({ name: 1, score: -1 })
```
## 与其他模块集成
### 与 @esengine/database 集成
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository, createRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 使用 UserRepository
const userRepo = new UserRepository(mongo)
await userRepo.register({ username: 'john', password: '123456' })
// 使用通用仓库
const playerRepo = createRepository<Player>(mongo, 'players')
```
### 与 @esengine/transaction 集成
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { createMongoStorage, TransactionManager } from '@esengine/transaction'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 创建事务存储(共享连接)
const storage = createMongoStorage(mongo)
await storage.ensureIndexes()
const txManager = new TransactionManager({ storage })
```

View File

@@ -0,0 +1,228 @@
---
title: "Redis 连接"
description: "Redis 连接管理、自动重连、键前缀"
---
## 配置选项
```typescript
interface RedisConnectionConfig {
/** Redis 主机 */
host?: string
/** Redis 端口 */
port?: number
/** 认证密码 */
password?: string
/** 数据库编号 */
db?: number
/** 键前缀 */
keyPrefix?: string
/** 是否自动重连(默认 true */
autoReconnect?: boolean
/** 重连间隔(毫秒,默认 5000 */
reconnectInterval?: number
/** 最大重连次数(默认 10 */
maxReconnectAttempts?: number
}
```
## 完整示例
```typescript
import { createRedisConnection, RedisConnectionToken } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0,
keyPrefix: 'game:',
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// 事件监听
redis.on('connected', () => {
console.log('Redis 已连接')
})
redis.on('disconnected', () => {
console.log('Redis 已断开')
})
redis.on('error', (event) => {
console.error('Redis 错误:', event.error)
})
// 连接
await redis.connect()
// 检查状态
console.log('已连接:', redis.isConnected())
console.log('Ping:', await redis.ping())
```
## IRedisConnection 接口
```typescript
interface IRedisConnection {
/** 连接 ID */
readonly id: string
/** 连接状态 */
readonly state: ConnectionState
/** 建立连接 */
connect(): Promise<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取值 */
get(key: string): Promise<string | null>
/** 设置值(可选 TTL单位秒 */
set(key: string, value: string, ttl?: number): Promise<void>
/** 删除键 */
del(key: string): Promise<boolean>
/** 检查键是否存在 */
exists(key: string): Promise<boolean>
/** 设置过期时间(秒) */
expire(key: string, seconds: number): Promise<boolean>
/** 获取剩余过期时间(秒) */
ttl(key: string): Promise<number>
/** 获取原生客户端(高级用法) */
getNativeClient(): Redis
}
```
## 使用示例
### 基本操作
```typescript
// 设置值
await redis.set('user:1:name', 'John')
// 设置带过期时间的值1 小时)
await redis.set('session:abc123', 'user-data', 3600)
// 获取值
const name = await redis.get('user:1:name')
// 检查键是否存在
const exists = await redis.exists('user:1:name')
// 删除键
await redis.del('user:1:name')
// 获取剩余过期时间
const ttl = await redis.ttl('session:abc123')
```
### 键前缀
配置 `keyPrefix` 后,所有操作自动添加前缀:
```typescript
const redis = createRedisConnection({
host: 'localhost',
keyPrefix: 'game:'
})
// 实际操作的键是 'game:user:1'
await redis.set('user:1', 'data')
// 实际查询的键是 'game:user:1'
const data = await redis.get('user:1')
```
### 高级操作
使用原生客户端进行高级操作:
```typescript
const client = redis.getNativeClient()
// 使用 Pipeline
const pipeline = client.pipeline()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.set('key3', 'value3')
await pipeline.exec()
// 使用事务
const multi = client.multi()
multi.incr('counter')
multi.get('counter')
const results = await multi.exec()
// 使用 Lua 脚本
const result = await client.eval(
`return redis.call('get', KEYS[1])`,
1,
'mykey'
)
```
## 与事务系统集成
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
import { RedisStorage, TransactionManager } from '@esengine/transaction'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'tx:'
})
await redis.connect()
// 创建事务存储
const storage = new RedisStorage({
factory: () => redis.getNativeClient(),
prefix: 'tx:'
})
const txManager = new TransactionManager({ storage })
```
## 连接状态
```typescript
type ConnectionState =
| 'disconnected' // 未连接
| 'connecting' // 连接中
| 'connected' // 已连接
| 'disconnecting' // 断开中
| 'error' // 错误状态
```
## 事件
| 事件 | 描述 |
|------|------|
| `connected` | 连接成功 |
| `disconnected` | 连接断开 |
| `reconnecting` | 正在重连 |
| `reconnected` | 重连成功 |
| `error` | 发生错误 |

View File

@@ -0,0 +1,140 @@
---
title: "数据库仓库"
description: "Repository 模式的数据库操作层,支持 CRUD、分页、软删除"
---
`@esengine/database` 是 ESEngine 的数据库操作层,基于 Repository 模式提供类型安全的 CRUD 操作。
## 特性
- **Repository 模式** - 泛型 CRUD 操作,类型安全
- **分页查询** - 内置分页支持
- **软删除** - 可选的软删除与恢复
- **用户管理** - 开箱即用的 UserRepository
- **密码安全** - 使用 scrypt 的密码哈希工具
## 安装
```bash
npm install @esengine/database @esengine/database-drivers
```
## 快速开始
### 基本仓库
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { Repository, createRepository } from '@esengine/database'
// 定义实体
interface Player {
id: string
name: string
score: number
createdAt: Date
updatedAt: Date
}
// 创建连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 创建仓库
const playerRepo = createRepository<Player>(mongo, 'players')
// CRUD 操作
const player = await playerRepo.create({
name: 'John',
score: 0
})
const found = await playerRepo.findById(player.id)
await playerRepo.update(player.id, { score: 100 })
await playerRepo.delete(player.id)
```
### 自定义仓库
```typescript
import { Repository, BaseEntity } from '@esengine/database'
import type { IMongoConnection } from '@esengine/database-drivers'
interface Player extends BaseEntity {
name: string
score: number
rank?: string
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
return this.findMany({
where: { rank }
})
}
async incrementScore(playerId: string, amount: number): Promise<Player | null> {
const player = await this.findById(playerId)
if (!player) return null
return this.update(playerId, { score: player.score + amount })
}
}
// 使用
const playerRepo = new PlayerRepository(mongo)
const topPlayers = await playerRepo.findTopPlayers(5)
```
### 用户仓库
```typescript
import { UserRepository } from '@esengine/database'
const userRepo = new UserRepository(mongo)
// 注册新用户
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com'
})
// 认证
const authenticated = await userRepo.authenticate('john', 'securePassword123')
if (authenticated) {
console.log('登录成功:', authenticated.username)
}
// 修改密码
await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456')
// 角色管理
await userRepo.addRole(user.id, 'admin')
await userRepo.removeRole(user.id, 'admin')
// 查询用户
const admins = await userRepo.findByRole('admin')
const john = await userRepo.findByUsername('john')
```
## 文档
- [仓库 API](/modules/database/repository/) - Repository 详细 API
- [用户管理](/modules/database/user/) - UserRepository 用法
- [查询语法](/modules/database/query/) - 查询条件语法

View File

@@ -0,0 +1,185 @@
---
title: "查询语法"
description: "查询条件操作符和语法"
---
## 基本查询
### 精确匹配
```typescript
await repo.findMany({
where: {
name: 'John',
status: 'active'
}
})
```
### 使用操作符
```typescript
await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
}
})
```
## 查询操作符
| 操作符 | 描述 | 示例 |
|--------|------|------|
| `$eq` | 等于 | `{ score: { $eq: 100 } }` |
| `$ne` | 不等于 | `{ status: { $ne: 'banned' } }` |
| `$gt` | 大于 | `{ score: { $gt: 50 } }` |
| `$gte` | 大于等于 | `{ level: { $gte: 10 } }` |
| `$lt` | 小于 | `{ age: { $lt: 18 } }` |
| `$lte` | 小于等于 | `{ price: { $lte: 100 } }` |
| `$in` | 在数组中 | `{ rank: { $in: ['gold', 'platinum'] } }` |
| `$nin` | 不在数组中 | `{ status: { $nin: ['banned', 'suspended'] } }` |
| `$like` | 模式匹配 | `{ name: { $like: '%john%' } }` |
| `$regex` | 正则匹配 | `{ email: { $regex: '@gmail.com$' } }` |
## 逻辑操作符
### $or
```typescript
await repo.findMany({
where: {
$or: [
{ score: { $gte: 1000 } },
{ rank: 'legendary' }
]
}
})
```
### $and
```typescript
await repo.findMany({
where: {
$and: [
{ score: { $gte: 100 } },
{ score: { $lte: 500 } }
]
}
})
```
### 组合使用
```typescript
await repo.findMany({
where: {
status: 'active',
$or: [
{ rank: 'gold' },
{ score: { $gte: 1000 } }
]
}
})
```
## 模式匹配
### $like 语法
- `%` - 匹配任意字符序列
- `_` - 匹配单个字符
```typescript
// 以 'John' 开头
{ name: { $like: 'John%' } }
// 以 'son' 结尾
{ name: { $like: '%son' } }
// 包含 'oh'
{ name: { $like: '%oh%' } }
// 第二个字符是 'o'
{ name: { $like: '_o%' } }
```
### $regex 语法
使用标准正则表达式:
```typescript
// 以 'John' 开头(大小写不敏感)
{ name: { $regex: '^john' } }
// Gmail 邮箱
{ email: { $regex: '@gmail\\.com$' } }
// 包含数字
{ username: { $regex: '\\d+' } }
```
## 排序
```typescript
await repo.findMany({
sort: {
score: 'desc', // 降序
name: 'asc' // 升序
}
})
```
## 分页
### 使用 limit/offset
```typescript
// 第一页
await repo.findMany({
limit: 20,
offset: 0
})
// 第二页
await repo.findMany({
limit: 20,
offset: 20
})
```
### 使用 findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 2, pageSize: 20 },
{ sort: { createdAt: 'desc' } }
)
```
## 完整示例
```typescript
// 查找活跃的金牌玩家,分数在 100-1000 之间
// 按分数降序排列,取前 10 个
const players = await repo.findMany({
where: {
status: 'active',
rank: 'gold',
score: { $gte: 100, $lte: 1000 }
},
sort: { score: 'desc' },
limit: 10
})
// 搜索用户名包含 'john' 或邮箱是 gmail 的用户
const users = await repo.findMany({
where: {
$or: [
{ username: { $like: '%john%' } },
{ email: { $regex: '@gmail\\.com$' } }
]
}
})
```

View File

@@ -0,0 +1,244 @@
---
title: "Repository API"
description: "泛型仓库接口CRUD 操作、分页、软删除"
---
## 创建仓库
### 使用工厂函数
```typescript
import { createRepository } from '@esengine/database'
const playerRepo = createRepository<Player>(mongo, 'players')
// 启用软删除
const playerRepo = createRepository<Player>(mongo, 'players', true)
```
### 继承 Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
interface Player extends BaseEntity {
name: string
score: number
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players', false) // 第三个参数:启用软删除
}
// 添加自定义方法
async findTopPlayers(limit: number): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
}
```
## BaseEntity 接口
所有实体必须继承 `BaseEntity`
```typescript
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
deletedAt?: Date // 软删除时使用
}
```
## 查询方法
### findById
```typescript
const player = await repo.findById('player-123')
```
### findOne
```typescript
const player = await repo.findOne({
where: { name: 'John' }
})
const topPlayer = await repo.findOne({
sort: { score: 'desc' }
})
```
### findMany
```typescript
// 简单查询
const players = await repo.findMany({
where: { rank: 'gold' }
})
// 复杂查询
const players = await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
},
sort: { score: 'desc', name: 'asc' },
limit: 10,
offset: 0
})
```
### findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { rank: 'gold' },
sort: { score: 'desc' }
}
)
console.log(result.data) // Player[]
console.log(result.total) // 总数量
console.log(result.totalPages) // 总页数
console.log(result.hasNext) // 是否有下一页
console.log(result.hasPrev) // 是否有上一页
```
### count
```typescript
const count = await repo.count({
where: { rank: 'gold' }
})
```
### exists
```typescript
const exists = await repo.exists({
where: { email: 'john@example.com' }
})
```
## 创建方法
### create
```typescript
const player = await repo.create({
name: 'John',
score: 0
})
// 自动生成 id, createdAt, updatedAt
```
### createMany
```typescript
const players = await repo.createMany([
{ name: 'Alice', score: 100 },
{ name: 'Bob', score: 200 },
{ name: 'Carol', score: 150 }
])
```
## 更新方法
### update
```typescript
const updated = await repo.update('player-123', {
score: 200,
rank: 'gold'
})
// 自动更新 updatedAt
```
## 删除方法
### delete
```typescript
// 普通删除
await repo.delete('player-123')
// 软删除(如果启用)
// 实际是设置 deletedAt 字段
```
### deleteMany
```typescript
const count = await repo.deleteMany({
where: { score: { $lt: 10 } }
})
```
## 软删除
### 启用软删除
```typescript
const repo = createRepository<Player>(mongo, 'players', true)
```
### 查询行为
```typescript
// 默认排除软删除记录
const players = await repo.findMany()
// 包含软删除记录
const allPlayers = await repo.findMany({
includeSoftDeleted: true
})
```
### 恢复记录
```typescript
await repo.restore('player-123')
```
## QueryOptions
```typescript
interface QueryOptions<T> {
/** 查询条件 */
where?: WhereCondition<T>
/** 排序 */
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
/** 限制数量 */
limit?: number
/** 偏移量 */
offset?: number
/** 包含软删除记录(仅在启用软删除时有效) */
includeSoftDeleted?: boolean
}
```
## PaginatedResult
```typescript
interface PaginatedResult<T> {
data: T[]
total: number
page: number
pageSize: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
```

View File

@@ -0,0 +1,277 @@
---
title: "用户管理"
description: "UserRepository 用户注册、认证、角色管理"
---
## 概述
`UserRepository` 提供开箱即用的用户管理功能:
- 用户注册与认证
- 密码哈希(使用 scrypt
- 角色管理
- 账户状态管理
## 快速开始
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
const userRepo = new UserRepository(mongo)
```
## 用户注册
```typescript
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com', // 可选
displayName: 'John Doe', // 可选
roles: ['player'] // 可选,默认 []
})
console.log(user)
// {
// id: 'uuid-...',
// username: 'john',
// email: 'john@example.com',
// displayName: 'John Doe',
// roles: ['player'],
// status: 'active',
// createdAt: Date,
// updatedAt: Date
// }
```
**注意**`register` 返回的 `SafeUser` 不包含密码哈希。
## 用户认证
```typescript
const user = await userRepo.authenticate('john', 'securePassword123')
if (user) {
console.log('登录成功:', user.username)
} else {
console.log('用户名或密码错误')
}
```
## 密码管理
### 修改密码
```typescript
const success = await userRepo.changePassword(
userId,
'oldPassword123',
'newPassword456'
)
if (success) {
console.log('密码修改成功')
} else {
console.log('原密码错误')
}
```
### 重置密码
```typescript
// 管理员直接重置密码
const success = await userRepo.resetPassword(userId, 'newPassword123')
```
## 角色管理
### 添加角色
```typescript
await userRepo.addRole(userId, 'admin')
await userRepo.addRole(userId, 'moderator')
```
### 移除角色
```typescript
await userRepo.removeRole(userId, 'moderator')
```
### 查询角色
```typescript
// 查找所有管理员
const admins = await userRepo.findByRole('admin')
// 检查用户是否有某角色
const user = await userRepo.findById(userId)
const isAdmin = user?.roles.includes('admin')
```
## 查询用户
### 按用户名查找
```typescript
const user = await userRepo.findByUsername('john')
```
### 按邮箱查找
```typescript
const user = await userRepo.findByEmail('john@example.com')
```
### 按角色查找
```typescript
const admins = await userRepo.findByRole('admin')
```
### 使用继承的方法
```typescript
// 分页查询
const result = await userRepo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { status: 'active' },
sort: { createdAt: 'desc' }
}
)
// 复杂查询
const users = await userRepo.findMany({
where: {
status: 'active',
roles: { $in: ['admin', 'moderator'] }
}
})
```
## 账户状态
```typescript
type UserStatus = 'active' | 'inactive' | 'banned' | 'suspended'
```
### 更新状态
```typescript
await userRepo.update(userId, { status: 'banned' })
```
### 查询特定状态
```typescript
const activeUsers = await userRepo.findMany({
where: { status: 'active' }
})
const bannedUsers = await userRepo.findMany({
where: { status: 'banned' }
})
```
## 类型定义
### UserEntity
```typescript
interface UserEntity extends BaseEntity {
username: string
passwordHash: string
email?: string
displayName?: string
roles: string[]
status: UserStatus
lastLoginAt?: Date
}
```
### SafeUser
```typescript
type SafeUser = Omit<UserEntity, 'passwordHash'>
```
### CreateUserParams
```typescript
interface CreateUserParams {
username: string
password: string
email?: string
displayName?: string
roles?: string[]
}
```
## 密码工具
独立的密码工具函数:
```typescript
import { hashPassword, verifyPassword } from '@esengine/database'
// 哈希密码
const hash = await hashPassword('myPassword123')
// 验证密码
const isValid = await verifyPassword('myPassword123', hash)
```
### 安全说明
- 使用 Node.js 内置的 `scrypt` 算法
- 自动生成随机盐值
- 默认使用安全的迭代参数
- 哈希格式:`salt:hash`(均为 hex 编码)
## 扩展 UserRepository
```typescript
import { UserRepository, UserEntity } from '@esengine/database'
interface GameUser extends UserEntity {
level: number
experience: number
coins: number
}
class GameUserRepository extends UserRepository {
// 重写集合名
constructor(connection: IMongoConnection) {
super(connection, 'game_users')
}
// 添加游戏相关方法
async addExperience(userId: string, amount: number): Promise<GameUser | null> {
const user = await this.findById(userId) as GameUser | null
if (!user) return null
const newExp = user.experience + amount
const newLevel = Math.floor(newExp / 1000) + 1
return this.update(userId, {
experience: newExp,
level: newLevel
}) as Promise<GameUser | null>
}
async findTopPlayers(limit: number = 10): Promise<GameUser[]> {
return this.findMany({
sort: { level: 'desc', experience: 'desc' },
limit
}) as Promise<GameUser[]>
}
}
```

View File

@@ -37,6 +37,13 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
| [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 |
### 数据库模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [数据库驱动](/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB、Redis 连接管理 |
| [数据库仓库](/modules/database/) | `@esengine/database` | Repository 模式数据操作 |
## 安装
所有模块都可以独立安装:

View File

@@ -92,6 +92,355 @@ const token = jwtProvider.sign({
const payload = jwtProvider.decode(token)
```
### 自定义提供者
你可以通过实现 `IAuthProvider` 接口来创建自定义认证提供者,以集成任何认证系统(如 OAuth、LDAP、自定义数据库认证等
#### IAuthProvider 接口
```typescript
interface IAuthProvider<TUser = unknown, TCredentials = unknown> {
/** 提供者名称 */
readonly name: string;
/** 验证凭证 */
verify(credentials: TCredentials): Promise<AuthResult<TUser>>;
/** 刷新令牌(可选) */
refresh?(token: string): Promise<AuthResult<TUser>>;
/** 撤销令牌(可选) */
revoke?(token: string): Promise<boolean>;
}
interface AuthResult<TUser> {
success: boolean;
user?: TUser;
error?: string;
errorCode?: AuthErrorCode;
token?: string;
expiresAt?: number;
}
type AuthErrorCode =
| 'INVALID_CREDENTIALS'
| 'EXPIRED_TOKEN'
| 'INVALID_TOKEN'
| 'USER_NOT_FOUND'
| 'ACCOUNT_DISABLED'
| 'RATE_LIMITED'
| 'INSUFFICIENT_PERMISSIONS';
```
#### 自定义提供者示例
**示例 1数据库密码认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface User {
id: string
username: string
roles: string[]
}
interface PasswordCredentials {
username: string
password: string
}
class DatabaseAuthProvider implements IAuthProvider<User, PasswordCredentials> {
readonly name = 'database'
async verify(credentials: PasswordCredentials): Promise<AuthResult<User>> {
const { username, password } = credentials
// 从数据库查询用户
const user = await db.users.findByUsername(username)
if (!user) {
return {
success: false,
error: '用户不存在',
errorCode: 'USER_NOT_FOUND'
}
}
// 验证密码(使用 bcrypt 等库)
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
return {
success: false,
error: '密码错误',
errorCode: 'INVALID_CREDENTIALS'
}
}
// 检查账号状态
if (user.disabled) {
return {
success: false,
error: '账号已禁用',
errorCode: 'ACCOUNT_DISABLED'
}
}
return {
success: true,
user: {
id: user.id,
username: user.username,
roles: user.roles
}
}
}
}
```
**示例 2OAuth/第三方认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface OAuthUser {
id: string
email: string
name: string
provider: string
roles: string[]
}
interface OAuthCredentials {
provider: 'google' | 'github' | 'discord'
accessToken: string
}
class OAuthProvider implements IAuthProvider<OAuthUser, OAuthCredentials> {
readonly name = 'oauth'
async verify(credentials: OAuthCredentials): Promise<AuthResult<OAuthUser>> {
const { provider, accessToken } = credentials
try {
// 根据提供商验证 token
const profile = await this.fetchUserProfile(provider, accessToken)
// 查找或创建本地用户
let user = await db.users.findByOAuth(provider, profile.id)
if (!user) {
user = await db.users.create({
oauthProvider: provider,
oauthId: profile.id,
email: profile.email,
name: profile.name,
roles: ['player']
})
}
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
provider,
roles: user.roles
}
}
} catch (error) {
return {
success: false,
error: 'OAuth 验证失败',
errorCode: 'INVALID_TOKEN'
}
}
}
private async fetchUserProfile(provider: string, token: string) {
switch (provider) {
case 'google':
return fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
case 'github':
return fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
// 其他提供商...
default:
throw new Error(`不支持的提供商: ${provider}`)
}
}
}
```
**示例 3API Key 认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface ApiUser {
id: string
name: string
roles: string[]
rateLimit: number
}
class ApiKeyAuthProvider implements IAuthProvider<ApiUser, string> {
readonly name = 'api-key'
private revokedKeys = new Set<string>()
async verify(apiKey: string): Promise<AuthResult<ApiUser>> {
if (!apiKey || !apiKey.startsWith('sk_')) {
return {
success: false,
error: 'API Key 格式无效',
errorCode: 'INVALID_TOKEN'
}
}
if (this.revokedKeys.has(apiKey)) {
return {
success: false,
error: 'API Key 已被撤销',
errorCode: 'INVALID_TOKEN'
}
}
// 从数据库查询 API Key
const keyData = await db.apiKeys.findByKey(apiKey)
if (!keyData) {
return {
success: false,
error: 'API Key 不存在',
errorCode: 'INVALID_CREDENTIALS'
}
}
// 检查过期
if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
return {
success: false,
error: 'API Key 已过期',
errorCode: 'EXPIRED_TOKEN'
}
}
return {
success: true,
user: {
id: keyData.userId,
name: keyData.name,
roles: keyData.roles,
rateLimit: keyData.rateLimit
},
expiresAt: keyData.expiresAt
}
}
async revoke(apiKey: string): Promise<boolean> {
this.revokedKeys.add(apiKey)
await db.apiKeys.revoke(apiKey)
return true
}
}
```
#### 使用自定义提供者
```typescript
import { createServer } from '@esengine/server'
import { withAuth } from '@esengine/server/auth'
// 创建自定义提供者
const dbAuthProvider = new DatabaseAuthProvider()
// 或使用 OAuth 提供者
const oauthProvider = new OAuthProvider()
// 使用自定义提供者
const server = withAuth(await createServer({ port: 3000 }), {
provider: dbAuthProvider, // 或 oauthProvider
// 从 WebSocket 连接请求中提取凭证
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
// 对于数据库认证:从查询参数获取
const username = url.searchParams.get('username')
const password = url.searchParams.get('password')
if (username && password) {
return { username, password }
}
// 对于 OAuth从 token 参数获取
const provider = url.searchParams.get('provider')
const accessToken = url.searchParams.get('access_token')
if (provider && accessToken) {
return { provider, accessToken }
}
// 对于 API Key从请求头获取
const apiKey = req.headers['x-api-key']
if (apiKey) {
return apiKey as string
}
return null
},
onAuthFailure: (conn, error) => {
console.log(`认证失败: ${error.errorCode} - ${error.error}`)
}
})
await server.start()
```
#### 组合多个提供者
你可以创建一个复合提供者来支持多种认证方式:
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface MultiAuthCredentials {
type: 'jwt' | 'oauth' | 'apikey' | 'password'
data: unknown
}
class MultiAuthProvider implements IAuthProvider<User, MultiAuthCredentials> {
readonly name = 'multi'
constructor(
private jwtProvider: JwtAuthProvider<User>,
private oauthProvider: OAuthProvider,
private apiKeyProvider: ApiKeyAuthProvider,
private dbProvider: DatabaseAuthProvider
) {}
async verify(credentials: MultiAuthCredentials): Promise<AuthResult<User>> {
switch (credentials.type) {
case 'jwt':
return this.jwtProvider.verify(credentials.data as string)
case 'oauth':
return this.oauthProvider.verify(credentials.data as OAuthCredentials)
case 'apikey':
return this.apiKeyProvider.verify(credentials.data as string)
case 'password':
return this.dbProvider.verify(credentials.data as PasswordCredentials)
default:
return {
success: false,
error: '不支持的认证类型',
errorCode: 'INVALID_CREDENTIALS'
}
}
}
}
```
### Session 提供者
使用服务端会话实现有状态认证:

View File

@@ -79,10 +79,140 @@ await server.start()
| `tickRate` | `number` | `20` | 全局 Tick 频率 (Hz) |
| `apiDir` | `string` | `'src/api'` | API 处理器目录 |
| `msgDir` | `string` | `'src/msg'` | 消息处理器目录 |
| `httpDir` | `string` | `'src/http'` | HTTP 路由目录 |
| `httpPrefix` | `string` | `'/api'` | HTTP 路由前缀 |
| `cors` | `boolean \| CorsOptions` | - | CORS 配置 |
| `onStart` | `(port) => void` | - | 启动回调 |
| `onConnect` | `(conn) => void` | - | 连接回调 |
| `onDisconnect` | `(conn) => void` | - | 断开回调 |
## HTTP 路由
支持 HTTP API 与 WebSocket 共用端口,适用于登录、注册等场景。
### 文件路由
`httpDir` 目录下创建路由文件,自动映射为 HTTP 端点:
```
src/http/
├── login.ts → POST /api/login
├── register.ts → POST /api/register
├── health.ts → GET /api/health (需设置 method: 'GET')
└── users/
└── [id].ts → POST /api/users/:id (动态路由)
```
### 定义路由
使用 `defineHttp` 定义类型安全的路由处理器:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server'
interface LoginBody {
username: string
password: string
}
export default defineHttp<LoginBody>({
method: 'POST', // 默认 POST可选 GET/PUT/DELETE/PATCH
handler(req, res) {
const { username, password } = req.body
// 验证凭证...
if (!isValid(username, password)) {
res.error(401, 'Invalid credentials')
return
}
// 生成 token...
res.json({ token: '...', userId: '...' })
}
})
```
### 请求对象 (HttpRequest)
```typescript
interface HttpRequest {
raw: IncomingMessage // Node.js 原始请求
method: string // 请求方法
path: string // 请求路径
query: Record<string, string> // 查询参数
headers: Record<string, string | string[] | undefined>
body: unknown // 解析后的 JSON 请求体
ip: string // 客户端 IP
}
```
### 响应对象 (HttpResponse)
```typescript
interface HttpResponse {
raw: ServerResponse // Node.js 原始响应
status(code: number): HttpResponse // 设置状态码(链式)
header(name: string, value: string): HttpResponse // 设置头(链式)
json(data: unknown): void // 发送 JSON
text(data: string): void // 发送文本
error(code: number, message: string): void // 发送错误
}
```
### 使用示例
```typescript
// 完整的登录服务器示例
import { createServer, defineHttp } from '@esengine/server'
import { createJwtAuthProvider, withAuth } from '@esengine/server/auth'
const jwtProvider = createJwtAuthProvider({
secret: process.env.JWT_SECRET!,
expiresIn: 3600 * 24,
})
const server = await createServer({
port: 8080,
httpDir: 'src/http',
httpPrefix: '/api',
cors: true,
})
// 包装认证WebSocket 连接验证 token
const authServer = withAuth(server, {
provider: jwtProvider,
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
return url.searchParams.get('token')
},
})
await authServer.start()
// HTTP: http://localhost:8080/api/*
// WebSocket: ws://localhost:8080?token=xxx
```
### 内联路由
也可以直接在配置中定义路由(与文件路由合并,内联优先):
```typescript
const server = await createServer({
port: 8080,
http: {
'/health': {
GET: (req, res) => res.json({ status: 'ok' }),
},
'/webhook': async (req, res) => {
// 接受所有方法
await handleWebhook(req.body)
res.json({ received: true })
},
},
})
```
## Room 系统
Room 是游戏房间的基类,管理玩家和游戏状态。

View File

@@ -125,23 +125,24 @@ tx:data:{key} - 业务数据
## MongoStorage
MongoDB 存储,适用于需要持久化和复杂查询的场景。使用工厂模式实现惰性连接。
MongoDB 存储,适用于需要持久化和复杂查询的场景。使用 `@esengine/database-drivers` 的共享连接。
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
import { createMongoConnection } from '@esengine/database-drivers';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// 工厂模式:惰性连接,首次操作时才创建连接
const storage = new MongoStorage({
factory: async () => {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
return client;
},
database: 'game',
transactionCollection: 'transactions', // 事务日志集合
dataCollection: 'transaction_data', // 业务数据集合
lockCollection: 'transaction_locks', // 锁集合
// 创建共享连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// 使用共享连接创建存储
const storage = createMongoStorage(mongo, {
transactionCollection: 'transactions', // 事务日志集合(可选)
dataCollection: 'transaction_data', // 业务数据集合(可选)
lockCollection: 'transaction_locks', // 锁集合(可选)
});
// 创建索引(首次运行时执行)
@@ -149,11 +150,14 @@ await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
// 使用后关闭连接
// 关闭存储(不会关闭共享连接
await storage.close();
// 或使用 await using 自动关闭 (TypeScript 5.2+)
await using storage = new MongoStorage({ ... });
// 共享连接可继续用于其他模块
const userRepo = new UserRepository(mongo); // @esengine/database
// 最后关闭共享连接
await mongo.disconnect();
```
### 特点

View File

@@ -74,6 +74,7 @@
"lint:fix": "turbo run lint:fix",
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",
"build:rapier2d": "node scripts/build-rapier2d.mjs",
"copy-modules": "node scripts/copy-engine-modules.mjs"
},
"author": "yhh",

View File

@@ -8,12 +8,23 @@ Before running the editor, ensure you have the following installed:
- **Node.js** >= 18.x
- **pnpm** >= 10.x
- **Rust** >= 1.70 (for Tauri)
- **Rust** >= 1.70 (for Tauri and WASM builds)
- **wasm-pack** (for building Rapier2D physics engine)
- **Platform-specific dependencies**:
- **Windows**: Microsoft Visual Studio C++ Build Tools
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Linux**: See [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites)
### Installing wasm-pack
```bash
# Using cargo
cargo install wasm-pack
# Or using the official installer script (Linux/macOS)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
## Quick Start
### 1. Clone and Install
@@ -24,7 +35,23 @@ cd esengine
pnpm install
```
### 2. Build Dependencies
### 2. Build Rapier2D WASM
The editor depends on Rapier2D physics engine WASM artifacts. First-time setup only requires one command:
```bash
pnpm build:rapier2d
```
This command automatically:
1. Prepares the Rust project
2. Builds WASM
3. Copies artifacts to `packages/physics/rapier2d/pkg`
4. Generates TypeScript source code
> **Note**: Requires Rust and wasm-pack to be installed.
### 3. Build Editor
From the project root:
@@ -32,7 +59,7 @@ From the project root:
pnpm build:editor
```
### 3. Run Editor
### 4. Run Editor
```bash
cd packages/editor/editor-app
@@ -43,6 +70,8 @@ pnpm tauri:dev
| Script | Description |
|--------|-------------|
| `pnpm build:rapier2d` | Build Rapier2D WASM (required for first-time setup) |
| `pnpm build:editor` | Build editor and all dependencies |
| `pnpm tauri:dev` | Run editor in development mode with hot-reload |
| `pnpm tauri:build` | Build production application |
| `pnpm build:sdk` | Build editor-runtime SDK |
@@ -62,6 +91,17 @@ editor-app/
## Troubleshooting
### Rapier2D WASM Build Failed
**Error**: `Could not resolve "../pkg/rapier_wasm2d"`
**Cause**: Missing Rapier2D WASM artifacts.
**Solution**:
1. Ensure `wasm-pack` is installed: `cargo install wasm-pack`
2. Run `pnpm build:rapier2d`
3. Verify `packages/physics/rapier2d/pkg/` directory exists and contains `rapier_wasm2d_bg.wasm` file
### Build Errors
```bash
@@ -76,6 +116,12 @@ pnpm build:editor
rustup update
```
### Windows Users Building WASM
The `pnpm build:rapier2d` script works directly on Windows. If you encounter issues:
1. Use Git Bash or WSL
2. Or download pre-built WASM artifacts from [Releases](https://github.com/esengine/esengine/releases)
## Documentation
- [ESEngine Documentation](https://esengine.cn/)

View File

@@ -8,12 +8,23 @@
- **Node.js** >= 18.x
- **pnpm** >= 10.x
- **Rust** >= 1.70 (Tauri 需要)
- **Rust** >= 1.70 (Tauri 和 WASM 构建需要)
- **wasm-pack** (构建 Rapier2D 物理引擎需要)
- **平台相关依赖**
- **Windows**: Microsoft Visual Studio C++ Build Tools
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Linux**: 参考 [Tauri 环境配置](https://tauri.app/v1/guides/getting-started/prerequisites)
### 安装 wasm-pack
```bash
# 使用 cargo 安装
cargo install wasm-pack
# 或使用官方安装脚本 (Linux/macOS)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
## 快速开始
### 1. 克隆并安装
@@ -24,7 +35,23 @@ cd esengine
pnpm install
```
### 2. 构建依赖
### 2. 构建 Rapier2D WASM
编辑器依赖 Rapier2D 物理引擎的 WASM 产物。首次构建只需执行一条命令:
```bash
pnpm build:rapier2d
```
该命令会自动完成以下步骤:
1. 准备 Rust 项目
2. 构建 WASM
3. 复制产物到 `packages/physics/rapier2d/pkg`
4. 生成 TypeScript 源码
> **注意**:需要已安装 Rust 和 wasm-pack。
### 3. 构建编辑器
在项目根目录执行:
@@ -32,7 +59,7 @@ pnpm install
pnpm build:editor
```
### 3. 启动编辑器
### 4. 启动编辑器
```bash
cd packages/editor/editor-app
@@ -43,6 +70,8 @@ pnpm tauri:dev
| 脚本 | 说明 |
|------|------|
| `pnpm build:rapier2d` | 构建 Rapier2D WASM首次构建必须执行|
| `pnpm build:editor` | 构建编辑器及所有依赖 |
| `pnpm tauri:dev` | 开发模式运行编辑器(支持热重载)|
| `pnpm tauri:build` | 构建生产版本应用 |
| `pnpm build:sdk` | 构建 editor-runtime SDK |
@@ -62,6 +91,17 @@ editor-app/
## 常见问题
### Rapier2D WASM 构建失败
**错误**: `Could not resolve "../pkg/rapier_wasm2d"`
**原因**: 缺少 Rapier2D 的 WASM 产物。
**解决方案**:
1. 确保已安装 `wasm-pack``cargo install wasm-pack`
2. 执行 `pnpm build:rapier2d`
3. 确认 `packages/physics/rapier2d/pkg/` 目录存在且包含 `rapier_wasm2d_bg.wasm` 文件
### 构建错误
```bash
@@ -76,6 +116,12 @@ pnpm build:editor
rustup update
```
### Windows 用户构建 WASM
`pnpm build:rapier2d` 脚本在 Windows 上可以直接运行。如果遇到问题:
1. 使用 Git Bash 或 WSL
2. 或从 [Releases](https://github.com/esengine/esengine/releases) 下载预编译的 WASM 产物
## 文档
- [ESEngine 文档](https://esengine.cn/)

View File

@@ -8,7 +8,10 @@
"rootDir": "./src",
"jsx": "react-jsx",
"skipLibCheck": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"@esengine/asset-system": ["../../../engine/asset-system/src"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]

View File

@@ -2,8 +2,7 @@ import { defineConfig } from 'tsup';
import { editorOnlyPreset } from '../../../tools/build-config/src/presets/plugin-tsup';
export default defineConfig({
...editorOnlyPreset({
external: ['@esengine/asset-system']
}),
tsconfig: 'tsconfig.build.json'
...editorOnlyPreset({}),
tsconfig: 'tsconfig.build.json',
noExternal: ['@esengine/asset-system']
});

View File

@@ -1,5 +1,54 @@
# @esengine/behavior-tree
## 4.2.0
### Minor Changes
- [#408](https://github.com/esengine/esengine/pull/408) [`b9ea8d1`](https://github.com/esengine/esengine/commit/b9ea8d14cf38e1480f638c229f9ee150b65f0c60) Thanks [@esengine](https://github.com/esengine)! - feat: add action() and condition() methods to BehaviorTreeBuilder
Added new methods to support custom executor types directly in the builder:
- `action(implementationType, name?, config?)` - Use custom action executors registered via `@NodeExecutorMetadata`
- `condition(implementationType, name?, config?)` - Use custom condition executors
This provides a cleaner API for using custom node executors compared to the existing `executeAction()` which only supports blackboard functions.
Example:
```typescript
// Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat'
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
return TaskStatus.Success;
}
}
// Use in builder
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## 4.1.2
### Patch Changes
- [#406](https://github.com/esengine/esengine/pull/406) [`0de4527`](https://github.com/esengine/esengine/commit/0de45279e612c04ae9be7fbd65ce496e4797a43c) Thanks [@esengine](https://github.com/esengine)! - fix(behavior-tree): export NodeExecutorMetadata as value instead of type
Fixed the export of `NodeExecutorMetadata` decorator in `execution/index.ts`.
Previously it was exported as `export type { NodeExecutorMetadata }` which only
exported the type signature, not the actual function. This caused runtime errors
in Cocos Creator: "TypeError: (intermediate value) is not a function".
Changed to `export { NodeExecutorMetadata }` to properly export the decorator function.
## 4.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/behavior-tree",
"version": "4.1.1",
"version": "4.2.0",
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -29,7 +29,8 @@
"clean": "rimraf dist tsconfig.tsbuildinfo",
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"author": "yhh",
"license": "MIT",

View File

@@ -181,12 +181,73 @@ export class BehaviorTreeBuilder {
}
/**
* 添加执行动作
* 添加执行动作(通过黑板函数)
*
* @zh 使用黑板中的 action_{actionName} 函数执行动作
* @en Execute action using action_{actionName} function from blackboard
*
* @example
* ```typescript
* BehaviorTreeBuilder.create("AI")
* .defineBlackboardVariable("action_Attack", (entity) => TaskStatus.Success)
* .selector("Root")
* .executeAction("Attack")
* .end()
* .build();
* ```
*/
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
}
/**
* 添加自定义动作节点
*
* @zh 直接使用注册的执行器类型(通过 @NodeExecutorMetadata 装饰器注册的类)
* @en Use a registered executor type directly (class registered via @NodeExecutorMetadata decorator)
*
* @param implementationType - 执行器类型名称(@NodeExecutorMetadata 中的 implementationType
* @param name - 节点显示名称
* @param config - 节点配置参数
*
* @example
* ```typescript
* // 1. 定义自定义执行器
* @NodeExecutorMetadata({
* implementationType: 'AttackAction',
* nodeType: NodeType.Action,
* displayName: '攻击动作',
* category: 'Action'
* })
* class AttackAction implements INodeExecutor {
* execute(context: NodeExecutionContext): TaskStatus {
* console.log("执行攻击!");
* return TaskStatus.Success;
* }
* }
*
* // 2. 在行为树中使用
* BehaviorTreeBuilder.create("AI")
* .selector("Root")
* .action("AttackAction", "Attack")
* .end()
* .build();
* ```
*/
action(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addActionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加自定义条件节点
*
* @zh 直接使用注册的条件执行器类型
* @en Use a registered condition executor type directly
*/
condition(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addConditionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加黑板比较条件
*/

View File

@@ -5,7 +5,7 @@ export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
export { NodeMetadataRegistry } from './NodeMetadata';
export type { NodeMetadata, ConfigFieldDefinition } from './NodeMetadata';
export { NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
export * from './Executors';

View File

@@ -0,0 +1,57 @@
# @esengine/database-drivers
## 1.1.1
### Patch Changes
- [#412](https://github.com/esengine/esengine/pull/412) [`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20) Thanks [@esengine](https://github.com/esengine)! - fix: include dist directory in npm package
Previous 1.1.0 release was missing the compiled dist directory.
## 1.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - feat: add database layer architecture
Added new database packages with layered architecture:
**@esengine/database-drivers (Layer 1)**
- MongoDB connection with pool management, auto-reconnect, events
- Redis connection with auto-reconnect, key prefix
- Type-safe `IMongoCollection<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` with CRUD, pagination, soft delete
- `UserRepository` with registration, authentication, role management
- Password hashing utilities using scrypt
- Query operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$regex`
**@esengine/transaction**
- Refactored `MongoStorage` to use shared connection from `@esengine/database-drivers`
- Removed factory pattern in favor of shared connection (breaking change)
- Simplified API: `createMongoStorage(connection, options?)`
Example usage:
```typescript
import { createMongoConnection } from '@esengine/database-drivers';
import { UserRepository } from '@esengine/database';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// Create shared connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// Use for database operations
const userRepo = new UserRepository(mongo);
await userRepo.register({ username: 'john', password: '123456' });
// Use for transactions (same connection)
const storage = createMongoStorage(mongo);
const txManager = new TransactionManager({ storage });
```

View File

@@ -0,0 +1,23 @@
{
"id": "database-drivers",
"name": "@esengine/database-drivers",
"globalKey": "database-drivers",
"displayName": "Database Drivers",
"description": "数据库连接驱动,提供 MongoDB、Redis 等数据库的连接管理 | Database connection drivers with connection pooling for MongoDB, Redis, etc.",
"version": "1.0.0",
"category": "Infrastructure",
"icon": "Database",
"tags": ["database", "mongodb", "redis", "connection"],
"isCore": false,
"defaultEnabled": true,
"isEngineModule": false,
"canContainContent": false,
"platforms": ["server"],
"dependencies": [],
"exports": {
"components": [],
"systems": []
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -0,0 +1,48 @@
{
"name": "@esengine/database-drivers",
"version": "1.1.1",
"description": "Database connection drivers for ESEngine | ESEngine 数据库连接驱动",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"peerDependencies": {
"mongodb": ">=6.0.0",
"ioredis": ">=5.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
},
"ioredis": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^20.0.0",
"mongodb": "^6.12.0",
"ioredis": "^5.3.0",
"tsup": "^8.0.0",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,238 @@
/**
* @zh MongoDB 集合适配器
* @en MongoDB collection adapter
*
* @zh 将 MongoDB 原生 Collection 适配为简化接口
* @en Adapts native MongoDB Collection to simplified interface
*/
import type { Collection, Db } from 'mongodb'
import type {
DeleteResult,
FindOneAndUpdateOptions,
FindOptions,
IMongoCollection,
IMongoDatabase,
IndexOptions,
InsertManyResult,
InsertOneResult,
UpdateResult
} from '../interfaces/IMongoCollection.js'
/**
* @zh MongoDB 集合适配器
* @en MongoDB collection adapter
*/
export class MongoCollectionAdapter<T extends object> implements IMongoCollection<T> {
readonly name: string
constructor(private readonly _collection: Collection<T>) {
this.name = _collection.collectionName
}
// =========================================================================
// 查询 | Query
// =========================================================================
async findOne(filter: object, options?: FindOptions): Promise<T | null> {
const doc = await this._collection.findOne(
filter as Parameters<typeof this._collection.findOne>[0],
{
sort: options?.sort as Parameters<typeof this._collection.findOne>[1] extends { sort?: infer S } ? S : never,
projection: options?.projection
}
)
return doc ? this._stripId(doc) : null
}
async find(filter: object, options?: FindOptions): Promise<T[]> {
let cursor = this._collection.find(
filter as Parameters<typeof this._collection.find>[0]
)
if (options?.sort) {
cursor = cursor.sort(options.sort as Parameters<typeof cursor.sort>[0])
}
if (options?.skip) {
cursor = cursor.skip(options.skip)
}
if (options?.limit) {
cursor = cursor.limit(options.limit)
}
if (options?.projection) {
cursor = cursor.project(options.projection)
}
const docs = await cursor.toArray()
return docs.map(doc => this._stripId(doc))
}
async countDocuments(filter?: object): Promise<number> {
return this._collection.countDocuments(
(filter ?? {}) as Parameters<typeof this._collection.countDocuments>[0]
)
}
// =========================================================================
// 创建 | Create
// =========================================================================
async insertOne(doc: T): Promise<InsertOneResult> {
const result = await this._collection.insertOne(
doc as Parameters<typeof this._collection.insertOne>[0]
)
return {
insertedId: result.insertedId,
acknowledged: result.acknowledged
}
}
async insertMany(docs: T[]): Promise<InsertManyResult> {
const result = await this._collection.insertMany(
docs as Parameters<typeof this._collection.insertMany>[0]
)
return {
insertedCount: result.insertedCount,
insertedIds: result.insertedIds as Record<number, unknown>,
acknowledged: result.acknowledged
}
}
// =========================================================================
// 更新 | Update
// =========================================================================
async updateOne(filter: object, update: object): Promise<UpdateResult> {
const result = await this._collection.updateOne(
filter as Parameters<typeof this._collection.updateOne>[0],
update as Parameters<typeof this._collection.updateOne>[1]
)
return {
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
upsertedCount: result.upsertedCount,
upsertedId: result.upsertedId,
acknowledged: result.acknowledged
}
}
async updateMany(filter: object, update: object): Promise<UpdateResult> {
const result = await this._collection.updateMany(
filter as Parameters<typeof this._collection.updateMany>[0],
update as Parameters<typeof this._collection.updateMany>[1]
)
return {
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
upsertedCount: result.upsertedCount,
upsertedId: result.upsertedId,
acknowledged: result.acknowledged
}
}
async findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null> {
const result = await this._collection.findOneAndUpdate(
filter as Parameters<typeof this._collection.findOneAndUpdate>[0],
update as Parameters<typeof this._collection.findOneAndUpdate>[1],
{
returnDocument: options?.returnDocument ?? 'after',
upsert: options?.upsert
}
)
return result ? this._stripId(result) : null
}
// =========================================================================
// 删除 | Delete
// =========================================================================
async deleteOne(filter: object): Promise<DeleteResult> {
const result = await this._collection.deleteOne(
filter as Parameters<typeof this._collection.deleteOne>[0]
)
return {
deletedCount: result.deletedCount,
acknowledged: result.acknowledged
}
}
async deleteMany(filter: object): Promise<DeleteResult> {
const result = await this._collection.deleteMany(
filter as Parameters<typeof this._collection.deleteMany>[0]
)
return {
deletedCount: result.deletedCount,
acknowledged: result.acknowledged
}
}
// =========================================================================
// 索引 | Index
// =========================================================================
async createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string> {
return this._collection.createIndex(spec, options)
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
/**
* @zh 移除 MongoDB 的 _id 字段
* @en Remove MongoDB's _id field
*/
private _stripId<D extends object>(doc: D): D {
const { _id, ...rest } = doc as { _id?: unknown } & Record<string, unknown>
return rest as D
}
}
/**
* @zh MongoDB 数据库适配器
* @en MongoDB database adapter
*/
export class MongoDatabaseAdapter implements IMongoDatabase {
readonly name: string
private _collections = new Map<string, MongoCollectionAdapter<object>>()
constructor(private readonly _db: Db) {
this.name = _db.databaseName
}
collection<T extends object = object>(name: string): IMongoCollection<T> {
if (!this._collections.has(name)) {
const nativeCollection = this._db.collection<T>(name)
this._collections.set(
name,
new MongoCollectionAdapter(nativeCollection) as MongoCollectionAdapter<object>
)
}
return this._collections.get(name) as IMongoCollection<T>
}
async listCollections(): Promise<string[]> {
const collections = await this._db.listCollections().toArray()
return collections.map(c => c.name)
}
async dropCollection(name: string): Promise<boolean> {
try {
await this._db.dropCollection(name)
this._collections.delete(name)
return true
} catch {
return false
}
}
}

View File

@@ -0,0 +1,343 @@
/**
* @zh MongoDB 连接驱动
* @en MongoDB connection driver
*
* @zh 提供 MongoDB 数据库的连接管理、自动重连和事件通知
* @en Provides MongoDB connection management, auto-reconnect, and event notification
*/
import type { Db, MongoClient as MongoClientType, MongoClientOptions } from 'mongodb'
import { randomUUID } from 'crypto'
import {
ConnectionError,
type ConnectionEvent,
type ConnectionEventListener,
type ConnectionEventType,
type ConnectionState,
type IEventableConnection,
type MongoConnectionConfig
} from '../types.js'
import type { IMongoCollection, IMongoDatabase } from '../interfaces/IMongoCollection.js'
import { MongoDatabaseAdapter } from '../adapters/MongoCollectionAdapter.js'
/**
* @zh MongoDB 连接接口
* @en MongoDB connection interface
*/
export interface IMongoConnection extends IEventableConnection {
/**
* @zh 获取数据库接口
* @en Get database interface
*/
getDatabase(): IMongoDatabase
/**
* @zh 获取原生客户端(高级用法)
* @en Get native client (advanced usage)
*/
getNativeClient(): MongoClientType
/**
* @zh 获取原生数据库(高级用法)
* @en Get native database (advanced usage)
*/
getNativeDatabase(): Db
/**
* @zh 获取集合
* @en Get collection
*/
collection<T extends object = object>(name: string): IMongoCollection<T>
}
/**
* @zh MongoDB 连接实现
* @en MongoDB connection implementation
*
* @example
* ```typescript
* const mongo = new MongoConnection({
* uri: 'mongodb://localhost:27017',
* database: 'game',
* autoReconnect: true,
* })
*
* mongo.on('connected', () => console.log('Connected!'))
* mongo.on('error', (e) => console.error('Error:', e.error))
*
* await mongo.connect()
*
* const users = mongo.collection('users')
* await users.insertOne({ name: 'test' })
*
* await mongo.disconnect()
* ```
*/
export class MongoConnection implements IMongoConnection {
readonly id: string
private _state: ConnectionState = 'disconnected'
private _client: MongoClientType | null = null
private _db: Db | null = null
private _config: MongoConnectionConfig
private _listeners = new Map<ConnectionEventType, Set<ConnectionEventListener>>()
private _reconnectAttempts = 0
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null
constructor(config: MongoConnectionConfig) {
this.id = randomUUID()
this._config = {
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10,
...config
}
}
// =========================================================================
// 状态 | State
// =========================================================================
get state(): ConnectionState {
return this._state
}
isConnected(): boolean {
return this._state === 'connected' && this._client !== null
}
// =========================================================================
// 连接管理 | Connection Management
// =========================================================================
async connect(): Promise<void> {
if (this._state === 'connected') {
return
}
if (this._state === 'connecting') {
throw new ConnectionError('Connection already in progress')
}
this._state = 'connecting'
try {
const { MongoClient } = await import('mongodb')
const options: MongoClientOptions = {}
if (this._config.pool) {
if (this._config.pool.minSize) {
options.minPoolSize = this._config.pool.minSize
}
if (this._config.pool.maxSize) {
options.maxPoolSize = this._config.pool.maxSize
}
if (this._config.pool.acquireTimeout) {
options.waitQueueTimeoutMS = this._config.pool.acquireTimeout
}
if (this._config.pool.maxLifetime) {
options.maxIdleTimeMS = this._config.pool.maxLifetime
}
}
this._client = new MongoClient(this._config.uri, options)
await this._client.connect()
this._db = this._client.db(this._config.database)
this._state = 'connected'
this._reconnectAttempts = 0
this._emit('connected')
this._setupClientEvents()
} catch (error) {
this._state = 'error'
const connError = new ConnectionError(
`Failed to connect to MongoDB: ${(error as Error).message}`,
'CONNECTION_FAILED',
error as Error
)
this._emit('error', connError)
throw connError
}
}
async disconnect(): Promise<void> {
if (this._state === 'disconnected') {
return
}
this._clearReconnectTimer()
this._state = 'disconnecting'
try {
if (this._client) {
await this._client.close()
this._client = null
this._db = null
}
this._state = 'disconnected'
this._emit('disconnected')
} catch (error) {
this._state = 'error'
throw new ConnectionError(
`Failed to disconnect: ${(error as Error).message}`,
'CONNECTION_FAILED',
error as Error
)
}
}
async ping(): Promise<boolean> {
if (!this._db) {
return false
}
try {
await this._db.command({ ping: 1 })
return true
} catch {
return false
}
}
// =========================================================================
// 数据库访问 | Database Access
// =========================================================================
private _dbAdapter: MongoDatabaseAdapter | null = null
getDatabase(): IMongoDatabase {
if (!this._db) {
throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED')
}
if (!this._dbAdapter) {
this._dbAdapter = new MongoDatabaseAdapter(this._db)
}
return this._dbAdapter
}
getNativeDatabase(): Db {
if (!this._db) {
throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED')
}
return this._db
}
getNativeClient(): MongoClientType {
if (!this._client) {
throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED')
}
return this._client
}
collection<T extends object = object>(name: string): IMongoCollection<T> {
return this.getDatabase().collection<T>(name)
}
// =========================================================================
// 事件 | Events
// =========================================================================
on(event: ConnectionEventType, listener: ConnectionEventListener): void {
if (!this._listeners.has(event)) {
this._listeners.set(event, new Set())
}
this._listeners.get(event)!.add(listener)
}
off(event: ConnectionEventType, listener: ConnectionEventListener): void {
this._listeners.get(event)?.delete(listener)
}
once(event: ConnectionEventType, listener: ConnectionEventListener): void {
const wrapper: ConnectionEventListener = (e) => {
this.off(event, wrapper)
listener(e)
}
this.on(event, wrapper)
}
private _emit(type: ConnectionEventType, error?: Error): void {
const event: ConnectionEvent = {
type,
connectionId: this.id,
timestamp: Date.now(),
error
}
const listeners = this._listeners.get(type)
if (listeners) {
for (const listener of listeners) {
try {
listener(event)
} catch {
// Ignore listener errors
}
}
}
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
private _setupClientEvents(): void {
if (!this._client) return
this._client.on('close', () => {
if (this._state === 'connected') {
this._state = 'disconnected'
this._emit('disconnected')
this._scheduleReconnect()
}
})
this._client.on('error', (error) => {
this._emit('error', error)
})
}
private _scheduleReconnect(): void {
if (!this._config.autoReconnect) return
if (this._reconnectAttempts >= (this._config.maxReconnectAttempts ?? 10)) {
return
}
this._clearReconnectTimer()
this._emit('reconnecting')
this._reconnectTimer = setTimeout(async () => {
this._reconnectAttempts++
try {
await this.connect()
this._emit('reconnected')
} catch {
this._scheduleReconnect()
}
}, this._config.reconnectInterval ?? 5000)
}
private _clearReconnectTimer(): void {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
}
}
}
/**
* @zh 创建 MongoDB 连接
* @en Create MongoDB connection
*
* @example
* ```typescript
* const mongo = createMongoConnection({
* uri: process.env.MONGODB_URI!,
* database: 'game',
* })
* await mongo.connect()
* ```
*/
export function createMongoConnection(config: MongoConnectionConfig): MongoConnection {
return new MongoConnection(config)
}

View File

@@ -0,0 +1,300 @@
/**
* @zh Redis 连接驱动
* @en Redis connection driver
*
* @zh 提供 Redis 数据库的连接管理、自动重连和事件通知
* @en Provides Redis connection management, auto-reconnect, and event notification
*/
import type { Redis as RedisClientType, RedisOptions } from 'ioredis'
import { randomUUID } from 'crypto'
import {
ConnectionError,
type ConnectionEvent,
type ConnectionEventListener,
type ConnectionEventType,
type ConnectionState,
type IEventableConnection,
type RedisConnectionConfig
} from '../types.js'
/**
* @zh Redis 连接接口
* @en Redis connection interface
*/
export interface IRedisConnection extends IEventableConnection {
/**
* @zh 获取原生客户端
* @en Get native client
*/
getClient(): RedisClientType
/**
* @zh 获取键值
* @en Get value by key
*/
get(key: string): Promise<string | null>
/**
* @zh 设置键值
* @en Set key value
*/
set(key: string, value: string, ttl?: number): Promise<void>
/**
* @zh 删除键
* @en Delete key
*/
del(key: string): Promise<boolean>
/**
* @zh 检查键是否存在
* @en Check if key exists
*/
exists(key: string): Promise<boolean>
}
/**
* @zh Redis 连接实现
* @en Redis connection implementation
*
* @example
* ```typescript
* const redis = new RedisConnection({
* host: 'localhost',
* port: 6379,
* keyPrefix: 'game:',
* })
*
* await redis.connect()
*
* await redis.set('player:1:score', '100', 3600)
* const score = await redis.get('player:1:score')
*
* await redis.disconnect()
* ```
*/
export class RedisConnection implements IRedisConnection {
readonly id: string
private _state: ConnectionState = 'disconnected'
private _client: RedisClientType | null = null
private _config: RedisConnectionConfig
private _listeners = new Map<ConnectionEventType, Set<ConnectionEventListener>>()
constructor(config: RedisConnectionConfig) {
this.id = randomUUID()
this._config = {
host: 'localhost',
port: 6379,
autoReconnect: true,
...config
}
}
// =========================================================================
// 状态 | State
// =========================================================================
get state(): ConnectionState {
return this._state
}
isConnected(): boolean {
return this._state === 'connected' && this._client !== null
}
// =========================================================================
// 连接管理 | Connection Management
// =========================================================================
async connect(): Promise<void> {
if (this._state === 'connected') {
return
}
if (this._state === 'connecting') {
throw new ConnectionError('Connection already in progress')
}
this._state = 'connecting'
try {
const Redis = (await import('ioredis')).default
const options: RedisOptions = {
host: this._config.host,
port: this._config.port,
password: this._config.password,
db: this._config.db,
keyPrefix: this._config.keyPrefix,
retryStrategy: this._config.autoReconnect
? (times) => Math.min(times * 100, 3000)
: () => null,
lazyConnect: true
}
if (this._config.url) {
this._client = new Redis(this._config.url, options)
} else {
this._client = new Redis(options)
}
this._setupClientEvents()
await this._client.connect()
this._state = 'connected'
this._emit('connected')
} catch (error) {
this._state = 'error'
const connError = new ConnectionError(
`Failed to connect to Redis: ${(error as Error).message}`,
'CONNECTION_FAILED',
error as Error
)
this._emit('error', connError)
throw connError
}
}
async disconnect(): Promise<void> {
if (this._state === 'disconnected') {
return
}
this._state = 'disconnecting'
try {
if (this._client) {
await this._client.quit()
this._client = null
}
this._state = 'disconnected'
this._emit('disconnected')
} catch (error) {
this._state = 'error'
throw new ConnectionError(
`Failed to disconnect: ${(error as Error).message}`,
'CONNECTION_FAILED',
error as Error
)
}
}
async ping(): Promise<boolean> {
if (!this._client) {
return false
}
try {
const result = await this._client.ping()
return result === 'PONG'
} catch {
return false
}
}
// =========================================================================
// 数据操作 | Data Operations
// =========================================================================
getClient(): RedisClientType {
if (!this._client) {
throw new ConnectionError('Not connected to Redis', 'CONNECTION_CLOSED')
}
return this._client
}
async get(key: string): Promise<string | null> {
return this.getClient().get(key)
}
async set(key: string, value: string, ttl?: number): Promise<void> {
const client = this.getClient()
if (ttl) {
await client.setex(key, ttl, value)
} else {
await client.set(key, value)
}
}
async del(key: string): Promise<boolean> {
const result = await this.getClient().del(key)
return result > 0
}
async exists(key: string): Promise<boolean> {
const result = await this.getClient().exists(key)
return result > 0
}
// =========================================================================
// 事件 | Events
// =========================================================================
on(event: ConnectionEventType, listener: ConnectionEventListener): void {
if (!this._listeners.has(event)) {
this._listeners.set(event, new Set())
}
this._listeners.get(event)!.add(listener)
}
off(event: ConnectionEventType, listener: ConnectionEventListener): void {
this._listeners.get(event)?.delete(listener)
}
once(event: ConnectionEventType, listener: ConnectionEventListener): void {
const wrapper: ConnectionEventListener = (e) => {
this.off(event, wrapper)
listener(e)
}
this.on(event, wrapper)
}
private _emit(type: ConnectionEventType, error?: Error): void {
const event: ConnectionEvent = {
type,
connectionId: this.id,
timestamp: Date.now(),
error
}
const listeners = this._listeners.get(type)
if (listeners) {
for (const listener of listeners) {
try {
listener(event)
} catch {
// Ignore listener errors
}
}
}
}
private _setupClientEvents(): void {
if (!this._client) return
this._client.on('close', () => {
if (this._state === 'connected') {
this._state = 'disconnected'
this._emit('disconnected')
}
})
this._client.on('error', (error) => {
this._emit('error', error)
})
this._client.on('reconnecting', () => {
this._emit('reconnecting')
})
}
}
/**
* @zh 创建 Redis 连接
* @en Create Redis connection
*/
export function createRedisConnection(config: RedisConnectionConfig): RedisConnection {
return new RedisConnection(config)
}

View File

@@ -0,0 +1,29 @@
/**
* @zh 数据库驱动导出
* @en Database drivers export
*/
export {
MongoConnection,
createMongoConnection,
type IMongoConnection
} from './MongoConnection.js'
export {
RedisConnection,
createRedisConnection,
type IRedisConnection
} from './RedisConnection.js'
// Re-export interfaces
export type {
IMongoCollection,
IMongoDatabase,
InsertOneResult,
InsertManyResult,
UpdateResult,
DeleteResult,
FindOptions,
FindOneAndUpdateOptions,
IndexOptions
} from '../interfaces/IMongoCollection.js'

View File

@@ -0,0 +1,117 @@
/**
* @zh @esengine/database-drivers 数据库连接驱动
* @en @esengine/database-drivers Database Connection Drivers
*
* @zh 提供 MongoDB、Redis 等数据库的连接管理,支持连接池、自动重连和事件通知
* @en Provides connection management for MongoDB, Redis, etc. with pooling, auto-reconnect, and events
*
* @example
* ```typescript
* import {
* createMongoConnection,
* createRedisConnection,
* MongoConnectionToken,
* RedisConnectionToken,
* } from '@esengine/database-drivers'
*
* // 创建 MongoDB 连接
* const mongo = createMongoConnection({
* uri: 'mongodb://localhost:27017',
* database: 'game',
* pool: { minSize: 5, maxSize: 20 },
* autoReconnect: true,
* })
*
* mongo.on('connected', () => console.log('MongoDB connected'))
* mongo.on('error', (e) => console.error('Error:', e.error))
*
* await mongo.connect()
*
* // 直接使用
* const users = mongo.collection('users')
* await users.insertOne({ name: 'test' })
*
* // 或注册到服务容器供其他模块使用
* services.register(MongoConnectionToken, mongo)
*
* // 创建 Redis 连接
* const redis = createRedisConnection({
* host: 'localhost',
* port: 6379,
* keyPrefix: 'game:',
* })
*
* await redis.connect()
* await redis.set('session:123', 'data', 3600)
*
* // 断开连接
* await mongo.disconnect()
* await redis.disconnect()
* ```
*/
// =============================================================================
// Types | 类型
// =============================================================================
export type {
ConnectionState,
IConnection,
IEventableConnection,
ConnectionEventType,
ConnectionEventListener,
ConnectionEvent,
PoolConfig,
MongoConnectionConfig,
RedisConnectionConfig,
DatabaseErrorCode
} from './types.js'
export {
DatabaseError,
ConnectionError,
DuplicateKeyError
} from './types.js'
// =============================================================================
// Drivers | 驱动
// =============================================================================
export {
MongoConnection,
createMongoConnection,
type IMongoConnection
} from './drivers/index.js'
export {
RedisConnection,
createRedisConnection,
type IRedisConnection
} from './drivers/index.js'
// =============================================================================
// Interfaces | 接口
// =============================================================================
export type {
IMongoCollection,
IMongoDatabase,
InsertOneResult,
InsertManyResult,
UpdateResult,
DeleteResult,
FindOptions,
FindOneAndUpdateOptions,
IndexOptions
} from './drivers/index.js'
// =============================================================================
// Tokens | 服务令牌
// =============================================================================
export {
MongoConnectionToken,
RedisConnectionToken,
createServiceToken,
type ServiceToken
} from './tokens.js'

View File

@@ -0,0 +1,237 @@
/**
* @zh MongoDB 集合简化接口
* @en MongoDB collection simplified interface
*
* @zh 提供与 MongoDB 解耦的类型安全接口
* @en Provides type-safe interface decoupled from MongoDB
*/
// =============================================================================
// 查询结果 | Query Results
// =============================================================================
/**
* @zh 插入结果
* @en Insert result
*/
export interface InsertOneResult {
insertedId: unknown
acknowledged: boolean
}
/**
* @zh 批量插入结果
* @en Insert many result
*/
export interface InsertManyResult {
insertedCount: number
insertedIds: Record<number, unknown>
acknowledged: boolean
}
/**
* @zh 更新结果
* @en Update result
*/
export interface UpdateResult {
matchedCount: number
modifiedCount: number
upsertedCount: number
upsertedId?: unknown
acknowledged: boolean
}
/**
* @zh 删除结果
* @en Delete result
*/
export interface DeleteResult {
deletedCount: number
acknowledged: boolean
}
// =============================================================================
// 查询选项 | Query Options
// =============================================================================
/**
* @zh 排序方向
* @en Sort direction
*/
export type SortDirection = 1 | -1 | 'asc' | 'desc'
/**
* @zh 排序定义
* @en Sort definition
*/
export type Sort = Record<string, SortDirection>
/**
* @zh 查找选项
* @en Find options
*/
export interface FindOptions {
sort?: Sort
limit?: number
skip?: number
projection?: Record<string, 0 | 1>
}
/**
* @zh 查找并更新选项
* @en Find and update options
*/
export interface FindOneAndUpdateOptions {
returnDocument?: 'before' | 'after'
upsert?: boolean
}
/**
* @zh 索引选项
* @en Index options
*/
export interface IndexOptions {
unique?: boolean
sparse?: boolean
expireAfterSeconds?: number
name?: string
}
// =============================================================================
// 集合接口 | Collection Interface
// =============================================================================
/**
* @zh MongoDB 集合接口
* @en MongoDB collection interface
*
* @zh 简化的集合操作接口,与 MongoDB 原生类型解耦
* @en Simplified collection interface, decoupled from MongoDB native types
*/
export interface IMongoCollection<T extends object> {
/**
* @zh 集合名称
* @en Collection name
*/
readonly name: string
// =========================================================================
// 查询 | Query
// =========================================================================
/**
* @zh 查找单条记录
* @en Find one document
*/
findOne(filter: object, options?: FindOptions): Promise<T | null>
/**
* @zh 查找多条记录
* @en Find documents
*/
find(filter: object, options?: FindOptions): Promise<T[]>
/**
* @zh 统计记录数
* @en Count documents
*/
countDocuments(filter?: object): Promise<number>
// =========================================================================
// 创建 | Create
// =========================================================================
/**
* @zh 插入单条记录
* @en Insert one document
*/
insertOne(doc: T): Promise<InsertOneResult>
/**
* @zh 批量插入
* @en Insert many documents
*/
insertMany(docs: T[]): Promise<InsertManyResult>
// =========================================================================
// 更新 | Update
// =========================================================================
/**
* @zh 更新单条记录
* @en Update one document
*/
updateOne(filter: object, update: object): Promise<UpdateResult>
/**
* @zh 批量更新
* @en Update many documents
*/
updateMany(filter: object, update: object): Promise<UpdateResult>
/**
* @zh 查找并更新
* @en Find one and update
*/
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// =========================================================================
// 删除 | Delete
// =========================================================================
/**
* @zh 删除单条记录
* @en Delete one document
*/
deleteOne(filter: object): Promise<DeleteResult>
/**
* @zh 批量删除
* @en Delete many documents
*/
deleteMany(filter: object): Promise<DeleteResult>
// =========================================================================
// 索引 | Index
// =========================================================================
/**
* @zh 创建索引
* @en Create index
*/
createIndex(spec: Record<string, 1 | -1>, options?: IndexOptions): Promise<string>
}
/**
* @zh MongoDB 数据库接口
* @en MongoDB database interface
*/
export interface IMongoDatabase {
/**
* @zh 数据库名称
* @en Database name
*/
readonly name: string
/**
* @zh 获取集合
* @en Get collection
*/
collection<T extends object = object>(name: string): IMongoCollection<T>
/**
* @zh 列出所有集合
* @en List all collections
*/
listCollections(): Promise<string[]>
/**
* @zh 删除集合
* @en Drop collection
*/
dropCollection(name: string): Promise<boolean>
}

View File

@@ -0,0 +1,56 @@
/**
* @zh 数据库驱动服务令牌
* @en Database driver service tokens
*
* @zh 用于依赖注入的服务令牌定义
* @en Service token definitions for dependency injection
*/
import type { IMongoConnection } from './drivers/MongoConnection.js'
import type { IRedisConnection } from './drivers/RedisConnection.js'
// =============================================================================
// 服务令牌类型 | Service Token Type
// =============================================================================
/**
* @zh 服务令牌
* @en Service token
*/
export interface ServiceToken<T> {
readonly id: string
readonly _type?: T
}
/**
* @zh 创建服务令牌
* @en Create service token
*/
export function createServiceToken<T>(id: string): ServiceToken<T> {
return { id }
}
// =============================================================================
// 连接令牌 | Connection Tokens
// =============================================================================
/**
* @zh MongoDB 连接令牌
* @en MongoDB connection token
*
* @example
* ```typescript
* // 注册
* services.register(MongoConnectionToken, mongoConnection)
*
* // 获取
* const mongo = services.get(MongoConnectionToken)
* ```
*/
export const MongoConnectionToken = createServiceToken<IMongoConnection>('database:mongo')
/**
* @zh Redis 连接令牌
* @en Redis connection token
*/
export const RedisConnectionToken = createServiceToken<IRedisConnection>('database:redis')

View File

@@ -0,0 +1,338 @@
/**
* @zh 数据库驱动核心类型定义
* @en Database driver core type definitions
*/
// =============================================================================
// 连接状态 | Connection State
// =============================================================================
/**
* @zh 连接状态
* @en Connection state
*/
export type ConnectionState =
| 'disconnected' // 未连接 | Not connected
| 'connecting' // 连接中 | Connecting
| 'connected' // 已连接 | Connected
| 'disconnecting' // 断开中 | Disconnecting
| 'error' // 错误 | Error
// =============================================================================
// 基础连接接口 | Base Connection Interface
// =============================================================================
/**
* @zh 数据库连接基础接口
* @en Base database connection interface
*/
export interface IConnection {
/**
* @zh 连接唯一标识
* @en Connection unique identifier
*/
readonly id: string
/**
* @zh 当前连接状态
* @en Current connection state
*/
readonly state: ConnectionState
/**
* @zh 建立连接
* @en Establish connection
*/
connect(): Promise<void>
/**
* @zh 断开连接
* @en Disconnect
*/
disconnect(): Promise<void>
/**
* @zh 检查是否已连接
* @en Check if connected
*/
isConnected(): boolean
/**
* @zh 健康检查
* @en Health check
*/
ping(): Promise<boolean>
}
// =============================================================================
// 连接事件 | Connection Events
// =============================================================================
/**
* @zh 连接事件类型
* @en Connection event types
*/
export type ConnectionEventType =
| 'connected'
| 'disconnected'
| 'error'
| 'reconnecting'
| 'reconnected'
/**
* @zh 连接事件监听器
* @en Connection event listener
*/
export type ConnectionEventListener = (event: ConnectionEvent) => void
/**
* @zh 连接事件
* @en Connection event
*/
export interface ConnectionEvent {
/**
* @zh 事件类型
* @en Event type
*/
type: ConnectionEventType
/**
* @zh 连接 ID
* @en Connection ID
*/
connectionId: string
/**
* @zh 时间戳
* @en Timestamp
*/
timestamp: number
/**
* @zh 错误信息(如果有)
* @en Error message (if any)
*/
error?: Error
}
/**
* @zh 可监听事件的连接接口
* @en Connection interface with event support
*/
export interface IEventableConnection extends IConnection {
/**
* @zh 添加事件监听
* @en Add event listener
*/
on(event: ConnectionEventType, listener: ConnectionEventListener): void
/**
* @zh 移除事件监听
* @en Remove event listener
*/
off(event: ConnectionEventType, listener: ConnectionEventListener): void
/**
* @zh 一次性事件监听
* @en One-time event listener
*/
once(event: ConnectionEventType, listener: ConnectionEventListener): void
}
// =============================================================================
// 连接池配置 | Connection Pool Configuration
// =============================================================================
/**
* @zh 连接池配置
* @en Connection pool configuration
*/
export interface PoolConfig {
/**
* @zh 最小连接数
* @en Minimum connections
*/
minSize?: number
/**
* @zh 最大连接数
* @en Maximum connections
*/
maxSize?: number
/**
* @zh 获取连接超时时间(毫秒)
* @en Acquire connection timeout in milliseconds
*/
acquireTimeout?: number
/**
* @zh 空闲连接超时时间(毫秒)
* @en Idle connection timeout in milliseconds
*/
idleTimeout?: number
/**
* @zh 连接最大生存时间(毫秒)
* @en Maximum connection lifetime in milliseconds
*/
maxLifetime?: number
}
// =============================================================================
// 数据库特定配置 | Database Specific Configuration
// =============================================================================
/**
* @zh MongoDB 连接配置
* @en MongoDB connection configuration
*/
export interface MongoConnectionConfig {
/**
* @zh 连接字符串
* @en Connection string
*
* @example "mongodb://localhost:27017"
* @example "mongodb+srv://user:pass@cluster.mongodb.net"
*/
uri: string
/**
* @zh 数据库名称
* @en Database name
*/
database: string
/**
* @zh 连接池配置
* @en Pool configuration
*/
pool?: PoolConfig
/**
* @zh 自动重连
* @en Auto reconnect
*/
autoReconnect?: boolean
/**
* @zh 重连间隔(毫秒)
* @en Reconnect interval in milliseconds
*/
reconnectInterval?: number
/**
* @zh 最大重连次数
* @en Maximum reconnect attempts
*/
maxReconnectAttempts?: number
}
/**
* @zh Redis 连接配置
* @en Redis connection configuration
*/
export interface RedisConnectionConfig {
/**
* @zh 主机地址
* @en Host address
*/
host?: string
/**
* @zh 端口
* @en Port
*/
port?: number
/**
* @zh 密码
* @en Password
*/
password?: string
/**
* @zh 数据库索引
* @en Database index
*/
db?: number
/**
* @zh 连接字符串(优先于其他配置)
* @en Connection URL (takes precedence over other options)
*/
url?: string
/**
* @zh 键前缀
* @en Key prefix
*/
keyPrefix?: string
/**
* @zh 自动重连
* @en Auto reconnect
*/
autoReconnect?: boolean
}
// =============================================================================
// 错误类型 | Error Types
// =============================================================================
/**
* @zh 数据库错误代码
* @en Database error codes
*/
export type DatabaseErrorCode =
| 'CONNECTION_FAILED'
| 'CONNECTION_TIMEOUT'
| 'CONNECTION_CLOSED'
| 'AUTHENTICATION_FAILED'
| 'POOL_EXHAUSTED'
| 'QUERY_FAILED'
| 'DUPLICATE_KEY'
| 'NOT_FOUND'
| 'VALIDATION_ERROR'
| 'UNKNOWN'
/**
* @zh 数据库错误
* @en Database error
*/
export class DatabaseError extends Error {
constructor(
message: string,
public readonly code: DatabaseErrorCode,
public readonly cause?: Error
) {
super(message)
this.name = 'DatabaseError'
}
}
/**
* @zh 连接错误
* @en Connection error
*/
export class ConnectionError extends DatabaseError {
constructor(message: string, code: DatabaseErrorCode = 'CONNECTION_FAILED', cause?: Error) {
super(message, code, cause)
this.name = 'ConnectionError'
}
}
/**
* @zh 重复键错误
* @en Duplicate key error
*/
export class DuplicateKeyError extends DatabaseError {
constructor(
message: string,
public readonly key: string,
cause?: Error
) {
super(message, 'DUPLICATE_KEY', cause)
this.name = 'DuplicateKeyError'
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declarationDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
external: ['mongodb', 'ioredis'],
treeshake: true,
});

View File

@@ -0,0 +1,65 @@
# @esengine/database
## 1.1.1
### Patch Changes
- [#412](https://github.com/esengine/esengine/pull/412) [`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20) Thanks [@esengine](https://github.com/esengine)! - fix: include dist directory in npm package
Previous 1.1.0 release was missing the compiled dist directory.
- Updated dependencies [[`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20)]:
- @esengine/database-drivers@1.1.1
## 1.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - feat: add database layer architecture
Added new database packages with layered architecture:
**@esengine/database-drivers (Layer 1)**
- MongoDB connection with pool management, auto-reconnect, events
- Redis connection with auto-reconnect, key prefix
- Type-safe `IMongoCollection<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` with CRUD, pagination, soft delete
- `UserRepository` with registration, authentication, role management
- Password hashing utilities using scrypt
- Query operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$regex`
**@esengine/transaction**
- Refactored `MongoStorage` to use shared connection from `@esengine/database-drivers`
- Removed factory pattern in favor of shared connection (breaking change)
- Simplified API: `createMongoStorage(connection, options?)`
Example usage:
```typescript
import { createMongoConnection } from '@esengine/database-drivers';
import { UserRepository } from '@esengine/database';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// Create shared connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// Use for database operations
const userRepo = new UserRepository(mongo);
await userRepo.register({ username: 'john', password: '123456' });
// Use for transactions (same connection)
const storage = createMongoStorage(mongo);
const txManager = new TransactionManager({ storage });
```
### Patch Changes
- Updated dependencies [[`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa)]:
- @esengine/database-drivers@1.1.0

View File

@@ -0,0 +1,23 @@
{
"id": "database",
"name": "@esengine/database",
"globalKey": "database",
"displayName": "Database",
"description": "数据库 CRUD 操作和仓库模式,支持用户管理、通用数据存储 | Database CRUD operations and repository pattern with user management and generic data storage",
"version": "1.0.0",
"category": "Infrastructure",
"icon": "Database",
"tags": ["database", "crud", "repository", "user"],
"isCore": false,
"defaultEnabled": true,
"isEngineModule": false,
"canContainContent": false,
"platforms": ["server"],
"dependencies": ["database-drivers"],
"exports": {
"components": [],
"systems": []
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -0,0 +1,37 @@
{
"name": "@esengine/database",
"version": "1.1.1",
"description": "Database CRUD operations and repositories for ESEngine | ESEngine 数据库 CRUD 操作和仓库",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/database-drivers": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,313 @@
/**
* @zh MongoDB 仓库实现
* @en MongoDB repository implementation
*
* @zh 基于 MongoDB 的通用仓库,支持 CRUD、分页、软删除
* @en Generic MongoDB repository with CRUD, pagination, and soft delete support
*/
import { randomUUID } from 'crypto'
import type { IMongoConnection, IMongoCollection } from '@esengine/database-drivers'
import type {
BaseEntity,
IRepository,
PaginatedResult,
PaginationParams,
QueryOptions,
WhereCondition
} from './types.js'
/**
* @zh MongoDB 仓库基类
* @en MongoDB repository base class
*
* @example
* ```typescript
* interface Player extends BaseEntity {
* name: string
* score: number
* }
*
* class PlayerRepository extends Repository<Player> {
* constructor(connection: IMongoConnection) {
* super(connection, 'players')
* }
*
* async findTopPlayers(limit: number): Promise<Player[]> {
* return this.findMany({
* sort: { score: 'desc' },
* limit,
* })
* }
* }
* ```
*/
export class Repository<T extends BaseEntity> implements IRepository<T> {
protected readonly _collection: IMongoCollection<T>
constructor(
protected readonly connection: IMongoConnection,
public readonly collectionName: string,
protected readonly enableSoftDelete: boolean = false
) {
this._collection = connection.collection<T>(collectionName)
}
// =========================================================================
// 查询 | Query
// =========================================================================
async findById(id: string): Promise<T | null> {
const filter = this._buildFilter({ where: { id } as WhereCondition<T> })
return this._collection.findOne(filter)
}
async findOne(options?: QueryOptions<T>): Promise<T | null> {
const filter = this._buildFilter(options)
const sort = this._buildSort(options)
return this._collection.findOne(filter, { sort })
}
async findMany(options?: QueryOptions<T>): Promise<T[]> {
const filter = this._buildFilter(options)
const sort = this._buildSort(options)
return this._collection.find(filter, {
sort,
skip: options?.offset,
limit: options?.limit
})
}
async findPaginated(
pagination: PaginationParams,
options?: Omit<QueryOptions<T>, 'limit' | 'offset'>
): Promise<PaginatedResult<T>> {
const { page, pageSize } = pagination
const offset = (page - 1) * pageSize
const [data, total] = await Promise.all([
this.findMany({ ...options, limit: pageSize, offset }),
this.count(options)
])
const totalPages = Math.ceil(total / pageSize)
return {
data,
total,
page,
pageSize,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
}
async count(options?: QueryOptions<T>): Promise<number> {
const filter = this._buildFilter(options)
return this._collection.countDocuments(filter)
}
async exists(options: QueryOptions<T>): Promise<boolean> {
const count = await this.count({ ...options, limit: 1 })
return count > 0
}
// =========================================================================
// 创建 | Create
// =========================================================================
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }): Promise<T> {
const now = new Date()
const entity = {
...data,
id: data.id || randomUUID(),
createdAt: now,
updatedAt: now
} as T
await this._collection.insertOne(entity)
return entity
}
async createMany(
data: Array<Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }>
): Promise<T[]> {
if (data.length === 0) return []
const now = new Date()
const entities = data.map(item => ({
...item,
id: item.id || randomUUID(),
createdAt: now,
updatedAt: now
})) as T[]
await this._collection.insertMany(entities)
return entities
}
// =========================================================================
// 更新 | Update
// =========================================================================
async update(
id: string,
data: Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>
): Promise<T | null> {
const filter = this._buildFilter({ where: { id } as WhereCondition<T> })
return this._collection.findOneAndUpdate(
filter,
{ $set: { ...data, updatedAt: new Date() } },
{ returnDocument: 'after' }
)
}
// =========================================================================
// 删除 | Delete
// =========================================================================
async delete(id: string): Promise<boolean> {
if (this.enableSoftDelete) {
const result = await this._collection.updateOne(
{ id },
{ $set: { deletedAt: new Date(), updatedAt: new Date() } }
)
return result.modifiedCount > 0
}
const result = await this._collection.deleteOne({ id })
return result.deletedCount > 0
}
async deleteMany(options: QueryOptions<T>): Promise<number> {
const filter = this._buildFilter(options)
if (this.enableSoftDelete) {
const result = await this._collection.updateMany(filter, {
$set: { deletedAt: new Date(), updatedAt: new Date() }
})
return result.modifiedCount
}
const result = await this._collection.deleteMany(filter)
return result.deletedCount
}
// =========================================================================
// 软删除恢复 | Soft Delete Recovery
// =========================================================================
/**
* @zh 恢复软删除的记录
* @en Restore soft deleted record
*/
async restore(id: string): Promise<T | null> {
if (!this.enableSoftDelete) {
throw new Error('Soft delete is not enabled for this repository')
}
return this._collection.findOneAndUpdate(
{ id, deletedAt: { $ne: null } },
{ $set: { deletedAt: null, updatedAt: new Date() } },
{ returnDocument: 'after' }
)
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
/**
* @zh 构建过滤条件
* @en Build filter
*/
protected _buildFilter(options?: QueryOptions<T>): object {
const filter: Record<string, unknown> = {}
if (this.enableSoftDelete && !options?.includeSoftDeleted) {
filter['deletedAt'] = null
}
if (!options?.where) {
return filter
}
return { ...filter, ...this._convertWhere(options.where) }
}
/**
* @zh 转换 where 条件
* @en Convert where condition
*/
protected _convertWhere(where: WhereCondition<T>): object {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(where)) {
if (key === '$or' && Array.isArray(value)) {
result['$or'] = value.map(v => this._convertWhere(v as WhereCondition<T>))
continue
}
if (key === '$and' && Array.isArray(value)) {
result['$and'] = value.map(v => this._convertWhere(v as WhereCondition<T>))
continue
}
if (value === undefined) continue
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const ops = value as Record<string, unknown>
const mongoOps: Record<string, unknown> = {}
if ('$eq' in ops) mongoOps['$eq'] = ops.$eq
if ('$ne' in ops) mongoOps['$ne'] = ops.$ne
if ('$gt' in ops) mongoOps['$gt'] = ops.$gt
if ('$gte' in ops) mongoOps['$gte'] = ops.$gte
if ('$lt' in ops) mongoOps['$lt'] = ops.$lt
if ('$lte' in ops) mongoOps['$lte'] = ops.$lte
if ('$in' in ops) mongoOps['$in'] = ops.$in
if ('$nin' in ops) mongoOps['$nin'] = ops.$nin
if ('$like' in ops) {
const pattern = (ops.$like as string).replace(/%/g, '.*').replace(/_/g, '.')
mongoOps['$regex'] = new RegExp(`^${pattern}$`, 'i')
}
if ('$regex' in ops) {
mongoOps['$regex'] = new RegExp(ops.$regex as string, 'i')
}
result[key] = Object.keys(mongoOps).length > 0 ? mongoOps : value
} else {
result[key] = value
}
}
return result
}
/**
* @zh 构建排序条件
* @en Build sort condition
*/
protected _buildSort(options?: QueryOptions<T>): Record<string, 1 | -1> | undefined {
if (!options?.sort) return undefined
const result: Record<string, 1 | -1> = {}
for (const [key, direction] of Object.entries(options.sort)) {
result[key] = direction === 'desc' ? -1 : 1
}
return result
}
}
/**
* @zh 创建仓库实例
* @en Create repository instance
*/
export function createRepository<T extends BaseEntity>(
connection: IMongoConnection,
collectionName: string,
enableSoftDelete = false
): Repository<T> {
return new Repository<T>(connection, collectionName, enableSoftDelete)
}

View File

@@ -0,0 +1,335 @@
/**
* @zh 用户仓库
* @en User repository
*
* @zh 提供用户管理的常用方法,包括注册、登录、角色管理等
* @en Provides common user management methods including registration, login, role management
*/
import type { IMongoConnection } from '@esengine/database-drivers'
import { Repository } from './Repository.js'
import { hashPassword, verifyPassword } from './password.js'
import type { UserEntity } from './types.js'
/**
* @zh 创建用户参数
* @en Create user parameters
*/
export interface CreateUserParams {
/**
* @zh 用户名
* @en Username
*/
username: string
/**
* @zh 明文密码
* @en Plain text password
*/
password: string
/**
* @zh 邮箱
* @en Email
*/
email?: string
/**
* @zh 角色列表
* @en Role list
*/
roles?: string[]
/**
* @zh 额外数据
* @en Additional metadata
*/
metadata?: Record<string, unknown>
}
/**
* @zh 用户信息(不含密码)
* @en User info (without password)
*/
export type SafeUser = Omit<UserEntity, 'passwordHash'>
/**
* @zh 用户仓库
* @en User repository
*
* @example
* ```typescript
* const mongo = createMongoConnection({ uri: '...', database: 'game' })
* await mongo.connect()
*
* const userRepo = new UserRepository(mongo)
*
* // 注册用户
* const user = await userRepo.register({
* username: 'player1',
* password: 'securePassword123',
* email: 'player1@example.com',
* })
*
* // 验证登录
* const result = await userRepo.authenticate('player1', 'securePassword123')
* if (result) {
* console.log('登录成功:', result.username)
* }
* ```
*/
export class UserRepository extends Repository<UserEntity> {
constructor(connection: IMongoConnection, collectionName = 'users') {
super(connection, collectionName, true)
}
// =========================================================================
// 查询 | Query
// =========================================================================
/**
* @zh 根据用户名查找用户
* @en Find user by username
*/
async findByUsername(username: string): Promise<UserEntity | null> {
return this.findOne({ where: { username } })
}
/**
* @zh 根据邮箱查找用户
* @en Find user by email
*/
async findByEmail(email: string): Promise<UserEntity | null> {
return this.findOne({ where: { email } })
}
/**
* @zh 检查用户名是否存在
* @en Check if username exists
*/
async usernameExists(username: string): Promise<boolean> {
return this.exists({ where: { username } })
}
/**
* @zh 检查邮箱是否存在
* @en Check if email exists
*/
async emailExists(email: string): Promise<boolean> {
return this.exists({ where: { email } })
}
// =========================================================================
// 注册与认证 | Registration & Authentication
// =========================================================================
/**
* @zh 注册新用户
* @en Register new user
*
* @param params - @zh 创建用户参数 @en Create user parameters
* @returns @zh 创建的用户(不含密码哈希)@en Created user (without password hash)
* @throws @zh 如果用户名已存在 @en If username already exists
*/
async register(params: CreateUserParams): Promise<SafeUser> {
const { username, password, email, roles, metadata } = params
if (await this.usernameExists(username)) {
throw new Error('Username already exists')
}
if (email && (await this.emailExists(email))) {
throw new Error('Email already exists')
}
const passwordHash = await hashPassword(password)
const user = await this.create({
username,
passwordHash,
email,
roles: roles ?? ['user'],
isActive: true,
metadata
})
return this.toSafeUser(user)
}
/**
* @zh 验证用户登录
* @en Authenticate user login
*
* @param username - @zh 用户名 @en Username
* @param password - @zh 明文密码 @en Plain text password
* @returns @zh 验证成功返回用户信息(不含密码),失败返回 null @en Returns user info on success, null on failure
*/
async authenticate(username: string, password: string): Promise<SafeUser | null> {
const user = await this.findByUsername(username)
if (!user || !user.isActive) {
return null
}
const isValid = await verifyPassword(password, user.passwordHash)
if (!isValid) {
return null
}
await this.update(user.id, { lastLoginAt: new Date() })
return this.toSafeUser(user)
}
// =========================================================================
// 密码管理 | Password Management
// =========================================================================
/**
* @zh 修改密码
* @en Change password
*
* @param userId - @zh 用户 ID @en User ID
* @param oldPassword - @zh 旧密码 @en Old password
* @param newPassword - @zh 新密码 @en New password
* @returns @zh 是否修改成功 @en Whether change was successful
*/
async changePassword(
userId: string,
oldPassword: string,
newPassword: string
): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const isValid = await verifyPassword(oldPassword, user.passwordHash)
if (!isValid) {
return false
}
const newHash = await hashPassword(newPassword)
const result = await this.update(userId, { passwordHash: newHash })
return result !== null
}
/**
* @zh 重置密码(管理员操作)
* @en Reset password (admin operation)
*
* @param userId - @zh 用户 ID @en User ID
* @param newPassword - @zh 新密码 @en New password
*/
async resetPassword(userId: string, newPassword: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const newHash = await hashPassword(newPassword)
const result = await this.update(userId, { passwordHash: newHash })
return result !== null
}
// =========================================================================
// 角色管理 | Role Management
// =========================================================================
/**
* @zh 添加角色
* @en Add role to user
*/
async addRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const roles = user.roles ?? []
if (!roles.includes(role)) {
roles.push(role)
await this.update(userId, { roles })
}
return true
}
/**
* @zh 移除角色
* @en Remove role from user
*/
async removeRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const roles = (user.roles ?? []).filter(r => r !== role)
await this.update(userId, { roles })
return true
}
/**
* @zh 检查用户是否拥有角色
* @en Check if user has role
*/
async hasRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
return user?.roles?.includes(role) ?? false
}
/**
* @zh 检查用户是否拥有任一角色
* @en Check if user has any of the roles
*/
async hasAnyRole(userId: string, roles: string[]): Promise<boolean> {
const user = await this.findById(userId)
if (!user?.roles) return false
return roles.some(role => user.roles.includes(role))
}
// =========================================================================
// 状态管理 | Status Management
// =========================================================================
/**
* @zh 禁用用户
* @en Deactivate user
*/
async deactivate(userId: string): Promise<boolean> {
const result = await this.update(userId, { isActive: false })
return result !== null
}
/**
* @zh 启用用户
* @en Activate user
*/
async activate(userId: string): Promise<boolean> {
const result = await this.update(userId, { isActive: true })
return result !== null
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
/**
* @zh 移除密码哈希
* @en Remove password hash
*/
private toSafeUser(user: UserEntity): SafeUser {
const { passwordHash, ...safeUser } = user
return safeUser
}
}
/**
* @zh 创建用户仓库
* @en Create user repository
*/
export function createUserRepository(
connection: IMongoConnection,
collectionName = 'users'
): UserRepository {
return new UserRepository(connection, collectionName)
}

View File

@@ -0,0 +1,152 @@
/**
* @zh @esengine/database 数据库操作层
* @en @esengine/database Database Operations Layer
*
* @zh 提供通用的数据库 CRUD 操作、仓库模式、用户管理等功能
* @en Provides generic database CRUD operations, repository pattern, user management
*
* @example
* ```typescript
* import { createMongoConnection } from '@esengine/database-drivers'
* import {
* Repository,
* UserRepository,
* createUserRepository,
* hashPassword,
* verifyPassword,
* } from '@esengine/database'
*
* // 1. 创建连接(来自 database-drivers
* const mongo = createMongoConnection({
* uri: 'mongodb://localhost:27017',
* database: 'game',
* })
* await mongo.connect()
*
* // 2. 使用用户仓库
* const userRepo = createUserRepository(mongo)
*
* // 注册
* const user = await userRepo.register({
* username: 'player1',
* password: 'securePassword123',
* })
*
* // 登录
* const authUser = await userRepo.authenticate('player1', 'securePassword123')
*
* // 3. 自定义仓库
* interface Player extends BaseEntity {
* name: string
* score: number
* level: number
* }
*
* class PlayerRepository extends Repository<Player> {
* constructor(connection: IMongoConnection) {
* super(connection, 'players')
* }
*
* async findTopPlayers(limit = 10): Promise<Player[]> {
* return this.findMany({
* sort: { score: 'desc' },
* limit,
* })
* }
*
* async addScore(playerId: string, points: number): Promise<Player | null> {
* const player = await this.findById(playerId)
* if (!player) return null
* return this.update(playerId, { score: player.score + points })
* }
* }
*
* // 4. 分页查询
* const result = await userRepo.findPaginated(
* { page: 1, pageSize: 20 },
* { where: { isActive: true }, sort: { createdAt: 'desc' } }
* )
* console.log(`第 ${result.page}/${result.totalPages} 页,共 ${result.total} 条`)
* ```
*/
// =============================================================================
// Types | 类型
// =============================================================================
export type {
BaseEntity,
SoftDeleteEntity,
ComparisonOperators,
WhereCondition,
SortDirection,
SortCondition,
QueryOptions,
PaginationParams,
PaginatedResult,
IRepository,
UserEntity
} from './types.js'
// =============================================================================
// Repository | 仓库
// =============================================================================
export { Repository, createRepository } from './Repository.js'
// =============================================================================
// User Repository | 用户仓库
// =============================================================================
export {
UserRepository,
createUserRepository,
type CreateUserParams,
type SafeUser
} from './UserRepository.js'
// =============================================================================
// Password | 密码工具
// =============================================================================
export {
hashPassword,
verifyPassword,
checkPasswordStrength,
type PasswordHashConfig,
type PasswordStrength,
type PasswordStrengthResult
} from './password.js'
// =============================================================================
// Tokens | 服务令牌
// =============================================================================
export {
MongoConnectionToken,
RedisConnectionToken,
UserRepositoryToken,
createServiceToken,
type ServiceToken
} from './tokens.js'
// =============================================================================
// Re-exports from database-drivers | 从 database-drivers 重新导出
// =============================================================================
export type {
IMongoConnection,
IRedisConnection,
MongoConnectionConfig,
RedisConnectionConfig,
ConnectionState,
DatabaseErrorCode
} from '@esengine/database-drivers'
export {
createMongoConnection,
createRedisConnection,
DatabaseError,
ConnectionError,
DuplicateKeyError
} from '@esengine/database-drivers'

View File

@@ -0,0 +1,189 @@
/**
* @zh 密码加密工具
* @en Password hashing utilities
*
* @zh 使用 Node.js 内置的 crypto 模块实现安全的密码哈希
* @en Uses Node.js built-in crypto module for secure password hashing
*/
import { randomBytes, scrypt, timingSafeEqual } from 'crypto'
import { promisify } from 'util'
const scryptAsync = promisify(scrypt)
/**
* @zh 密码哈希配置
* @en Password hash configuration
*/
export interface PasswordHashConfig {
/**
* @zh 盐的字节长度(默认 16
* @en Salt length in bytes (default 16)
*/
saltLength?: number
/**
* @zh scrypt 密钥长度(默认 64
* @en scrypt key length (default 64)
*/
keyLength?: number
}
const DEFAULT_CONFIG: Required<PasswordHashConfig> = {
saltLength: 16,
keyLength: 64
}
/**
* @zh 对密码进行哈希处理
* @en Hash a password
*
* @param password - @zh 明文密码 @en Plain text password
* @param config - @zh 哈希配置 @en Hash configuration
* @returns @zh 格式为 "salt:hash" 的哈希字符串 @en Hash string in "salt:hash" format
*
* @example
* ```typescript
* const hashedPassword = await hashPassword('myPassword123')
* // 存储 hashedPassword 到数据库
* ```
*/
export async function hashPassword(
password: string,
config?: PasswordHashConfig
): Promise<string> {
const { saltLength, keyLength } = { ...DEFAULT_CONFIG, ...config }
const salt = randomBytes(saltLength).toString('hex')
const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer
return `${salt}:${derivedKey.toString('hex')}`
}
/**
* @zh 验证密码是否正确
* @en Verify if a password is correct
*
* @param password - @zh 明文密码 @en Plain text password
* @param hashedPassword - @zh 存储的哈希密码 @en Stored hashed password
* @param config - @zh 哈希配置 @en Hash configuration
* @returns @zh 密码是否匹配 @en Whether the password matches
*
* @example
* ```typescript
* const isValid = await verifyPassword('myPassword123', storedHash)
* if (isValid) {
* // 登录成功
* }
* ```
*/
export async function verifyPassword(
password: string,
hashedPassword: string,
config?: PasswordHashConfig
): Promise<boolean> {
const { keyLength } = { ...DEFAULT_CONFIG, ...config }
const [salt, storedHash] = hashedPassword.split(':')
if (!salt || !storedHash) {
return false
}
try {
const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer
const storedBuffer = Buffer.from(storedHash, 'hex')
return timingSafeEqual(derivedKey, storedBuffer)
} catch {
return false
}
}
/**
* @zh 密码强度等级
* @en Password strength level
*/
export type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong'
/**
* @zh 密码强度检查结果
* @en Password strength check result
*/
export interface PasswordStrengthResult {
/**
* @zh 强度分数 (0-6)
* @en Strength score (0-6)
*/
score: number
/**
* @zh 强度等级
* @en Strength level
*/
level: PasswordStrength
/**
* @zh 改进建议
* @en Improvement suggestions
*/
feedback: string[]
}
/**
* @zh 检查密码强度
* @en Check password strength
*
* @param password - @zh 明文密码 @en Plain text password
* @returns @zh 密码强度信息 @en Password strength information
*/
export function checkPasswordStrength(password: string): PasswordStrengthResult {
const feedback: string[] = []
let score = 0
if (password.length >= 8) {
score += 1
} else {
feedback.push('Password should be at least 8 characters')
}
if (password.length >= 12) {
score += 1
}
if (/[a-z]/.test(password)) {
score += 1
} else {
feedback.push('Password should contain lowercase letters')
}
if (/[A-Z]/.test(password)) {
score += 1
} else {
feedback.push('Password should contain uppercase letters')
}
if (/[0-9]/.test(password)) {
score += 1
} else {
feedback.push('Password should contain numbers')
}
if (/[^a-zA-Z0-9]/.test(password)) {
score += 1
} else {
feedback.push('Password should contain special characters')
}
let level: PasswordStrength
if (score <= 2) {
level = 'weak'
} else if (score <= 3) {
level = 'fair'
} else if (score <= 4) {
level = 'good'
} else {
level = 'strong'
}
return { score, level, feedback }
}

View File

@@ -0,0 +1,17 @@
/**
* @zh 数据库服务令牌
* @en Database service tokens
*/
import type { ServiceToken, createServiceToken as createToken } from '@esengine/database-drivers'
import type { UserRepository } from './UserRepository.js'
// Re-export from database-drivers for convenience
export { MongoConnectionToken, RedisConnectionToken, createServiceToken } from '@esengine/database-drivers'
export type { ServiceToken } from '@esengine/database-drivers'
/**
* @zh 用户仓库令牌
* @en User repository token
*/
export const UserRepositoryToken: ServiceToken<UserRepository> = { id: 'database:userRepository' }

View File

@@ -0,0 +1,333 @@
/**
* @zh 数据库核心类型定义
* @en Database core type definitions
*/
// =============================================================================
// 实体类型 | Entity Types
// =============================================================================
/**
* @zh 基础实体接口
* @en Base entity interface
*/
export interface BaseEntity {
/**
* @zh 实体唯一标识
* @en Entity unique identifier
*/
id: string
/**
* @zh 创建时间
* @en Creation timestamp
*/
createdAt?: Date
/**
* @zh 更新时间
* @en Update timestamp
*/
updatedAt?: Date
}
/**
* @zh 软删除实体接口
* @en Soft delete entity interface
*/
export interface SoftDeleteEntity extends BaseEntity {
/**
* @zh 删除时间null 表示未删除)
* @en Deletion timestamp (null means not deleted)
*/
deletedAt?: Date | null
}
// =============================================================================
// 查询类型 | Query Types
// =============================================================================
/**
* @zh 比较操作符
* @en Comparison operators
*/
export interface ComparisonOperators<T> {
$eq?: T
$ne?: T
$gt?: T
$gte?: T
$lt?: T
$lte?: T
$in?: T[]
$nin?: T[]
$like?: string
$regex?: string
}
/**
* @zh 查询条件
* @en Query condition
*/
export type WhereCondition<T> = {
[K in keyof T]?: T[K] | ComparisonOperators<T[K]>
} & {
$or?: WhereCondition<T>[]
$and?: WhereCondition<T>[]
}
/**
* @zh 排序方向
* @en Sort direction
*/
export type SortDirection = 'asc' | 'desc'
/**
* @zh 排序条件
* @en Sort condition
*/
export type SortCondition<T> = {
[K in keyof T]?: SortDirection
}
/**
* @zh 查询选项
* @en Query options
*/
export interface QueryOptions<T> {
/**
* @zh 过滤条件
* @en Filter conditions
*/
where?: WhereCondition<T>
/**
* @zh 排序条件
* @en Sort conditions
*/
sort?: SortCondition<T>
/**
* @zh 限制返回数量
* @en Limit number of results
*/
limit?: number
/**
* @zh 跳过记录数
* @en Number of records to skip
*/
offset?: number
/**
* @zh 是否包含软删除记录
* @en Whether to include soft deleted records
*/
includeSoftDeleted?: boolean
}
// =============================================================================
// 分页类型 | Pagination Types
// =============================================================================
/**
* @zh 分页参数
* @en Pagination parameters
*/
export interface PaginationParams {
/**
* @zh 页码(从 1 开始)
* @en Page number (starts from 1)
*/
page: number
/**
* @zh 每页数量
* @en Items per page
*/
pageSize: number
}
/**
* @zh 分页结果
* @en Pagination result
*/
export interface PaginatedResult<T> {
/**
* @zh 数据列表
* @en Data list
*/
data: T[]
/**
* @zh 总记录数
* @en Total count
*/
total: number
/**
* @zh 当前页码
* @en Current page
*/
page: number
/**
* @zh 每页数量
* @en Page size
*/
pageSize: number
/**
* @zh 总页数
* @en Total pages
*/
totalPages: number
/**
* @zh 是否有下一页
* @en Whether has next page
*/
hasNext: boolean
/**
* @zh 是否有上一页
* @en Whether has previous page
*/
hasPrev: boolean
}
// =============================================================================
// 仓库接口 | Repository Interface
// =============================================================================
/**
* @zh 仓库接口
* @en Repository interface
*/
export interface IRepository<T extends BaseEntity> {
/**
* @zh 集合名称
* @en Collection name
*/
readonly collectionName: string
/**
* @zh 根据 ID 查找
* @en Find by ID
*/
findById(id: string): Promise<T | null>
/**
* @zh 查找单条记录
* @en Find one record
*/
findOne(options?: QueryOptions<T>): Promise<T | null>
/**
* @zh 查找多条记录
* @en Find many records
*/
findMany(options?: QueryOptions<T>): Promise<T[]>
/**
* @zh 分页查询
* @en Paginated query
*/
findPaginated(
pagination: PaginationParams,
options?: Omit<QueryOptions<T>, 'limit' | 'offset'>
): Promise<PaginatedResult<T>>
/**
* @zh 统计记录数
* @en Count records
*/
count(options?: QueryOptions<T>): Promise<number>
/**
* @zh 检查记录是否存在
* @en Check if record exists
*/
exists(options: QueryOptions<T>): Promise<boolean>
/**
* @zh 创建记录
* @en Create record
*/
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }): Promise<T>
/**
* @zh 批量创建
* @en Bulk create
*/
createMany(data: Array<Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }>): Promise<T[]>
/**
* @zh 更新记录
* @en Update record
*/
update(id: string, data: Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>): Promise<T | null>
/**
* @zh 删除记录
* @en Delete record
*/
delete(id: string): Promise<boolean>
/**
* @zh 批量删除
* @en Bulk delete
*/
deleteMany(options: QueryOptions<T>): Promise<number>
}
// =============================================================================
// 用户实体 | User Entity
// =============================================================================
/**
* @zh 用户实体
* @en User entity
*/
export interface UserEntity extends SoftDeleteEntity {
/**
* @zh 用户名
* @en Username
*/
username: string
/**
* @zh 密码哈希
* @en Password hash
*/
passwordHash: string
/**
* @zh 邮箱
* @en Email
*/
email?: string
/**
* @zh 用户角色
* @en User roles
*/
roles: string[]
/**
* @zh 是否启用
* @en Is active
*/
isActive: boolean
/**
* @zh 最后登录时间
* @en Last login timestamp
*/
lastLoginAt?: Date
/**
* @zh 额外数据
* @en Additional metadata
*/
metadata?: Record<string, unknown>
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declarationDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
external: ['@esengine/database-drivers'],
treeshake: true,
});

View File

@@ -1,5 +1,19 @@
# @esengine/network
## 5.0.3
### Patch Changes
- Updated dependencies [[`902c0a1`](https://github.com/esengine/esengine/commit/902c0a10749f80bd8f499b44154646379d359704)]:
- @esengine/rpc@1.1.3
## 5.0.2
### Patch Changes
- Updated dependencies []:
- @esengine/rpc@1.1.2
## 5.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/network",
"version": "5.0.1",
"version": "5.0.3",
"description": "Network synchronization for multiplayer games",
"esengine": {
"plugin": true,

View File

@@ -1,5 +1,93 @@
# @esengine/rpc
## 1.1.3
### Patch Changes
- [#404](https://github.com/esengine/esengine/pull/404) [`902c0a1`](https://github.com/esengine/esengine/commit/902c0a10749f80bd8f499b44154646379d359704) Thanks [@esengine](https://github.com/esengine)! - feat(server): add HTTP file-based routing support / 添加 HTTP 文件路由支持
New feature that allows organizing HTTP routes in separate files, similar to API and message handlers.
新功能:支持将 HTTP 路由组织在独立文件中,类似于 API 和消息处理器的文件路由方式。
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server';
export default defineHttp<{ username: string; password: string }>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body;
res.json({ token: '...', userId: '...' });
}
});
```
Server configuration / 服务器配置:
```typescript
const server = await createServer({
port: 8080,
httpDir: 'src/http', // HTTP routes directory / HTTP 路由目录
httpPrefix: '/api', // Route prefix / 路由前缀
cors: true
});
```
File naming convention / 文件命名规则:
- `login.ts` → POST /api/login
- `users/profile.ts` → POST /api/users/profile
- `users/[id].ts` → POST /api/users/:id (dynamic routes / 动态路由)
- Set `method: 'GET'` in defineHttp for GET requests / 在 defineHttp 中设置 `method: 'GET'` 以处理 GET 请求
Also includes / 还包括:
- `defineHttp<TBody>()` helper for type-safe route definitions / 类型安全的路由定义辅助函数
- Support for merging file routes with inline `http` config / 支持文件路由与内联 `http` 配置合并
- RPC server supports attaching to existing HTTP server via `server` option / RPC 服务器支持通过 `server` 选项附加到现有 HTTP 服务器
## 1.1.2
### Patch Changes
- feat(server): add HTTP file-based routing support
New feature that allows organizing HTTP routes in separate files, similar to API and message handlers:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server';
export default defineHttp<{ username: string; password: string }>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body;
// ... authentication logic
res.json({ token: '...', userId: '...' });
}
});
```
Server configuration:
```typescript
const server = await createServer({
port: 8080,
httpDir: 'src/http', // HTTP routes directory
httpPrefix: '/api', // Route prefix
cors: true
});
```
File naming convention:
- `login.ts` → POST /api/login
- `users/profile.ts` → POST /api/users/profile
- `users/[id].ts` → POST /api/users/:id (dynamic routes)
- Set `method: 'GET'` in defineHttp for GET requests
Also includes:
- `defineHttp<TBody>()` helper function for type-safe route definitions
- Support for merging file routes with inline `http` config
- RPC server now supports attaching to existing HTTP server via `server` option
## 1.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/rpc",
"version": "1.1.1",
"version": "1.1.3",
"description": "Elegant type-safe RPC library for ESEngine",
"type": "module",
"main": "./dist/index.js",

View File

@@ -4,6 +4,7 @@
*/
import { WebSocketServer, WebSocket } from 'ws'
import type { Server as HttpServer } from 'node:http'
import type {
ProtocolDef,
ApiNames,
@@ -66,10 +67,19 @@ type MsgHandlers<P extends ProtocolDef, TConnData> = {
*/
export interface ServeOptions<P extends ProtocolDef, TConnData = unknown> {
/**
* @zh 监听端口
* @en Listen port
* @zh 监听端口(与 server 二选一)
* @en Listen port (mutually exclusive with server)
*/
port: number
port?: number
/**
* @zh 已有的 HTTP 服务器(与 port 二选一)
* @en Existing HTTP server (mutually exclusive with port)
*
* @zh 使用此选项可以在同一端口同时支持 HTTP 和 WebSocket
* @en Use this option to support both HTTP and WebSocket on the same port
*/
server?: HttpServer
/**
* @zh API 处理器
@@ -280,7 +290,16 @@ export function serve<P extends ProtocolDef, TConnData = unknown>(
async start() {
return new Promise((resolve) => {
wss = new WebSocketServer({ port: options.port })
// 根据配置创建 WebSocketServer
if (options.server) {
// 附加到已有的 HTTP 服务器
wss = new WebSocketServer({ server: options.server })
} else if (options.port) {
// 独立创建
wss = new WebSocketServer({ port: options.port })
} else {
throw new Error('Either port or server must be provided')
}
wss.on('connection', async (ws, req) => {
const id = String(++connIdCounter)
@@ -318,10 +337,16 @@ export function serve<P extends ProtocolDef, TConnData = unknown>(
await options.onConnect?.(conn)
})
wss.on('listening', () => {
options.onStart?.(options.port)
// 如果使用已有的 HTTP 服务器WebSocketServer 不会触发 listening 事件
if (options.server) {
options.onStart?.(0) // 端口由 HTTP 服务器管理
resolve()
})
} else {
wss.on('listening', () => {
options.onStart?.(options.port!)
resolve()
})
}
})
},

View File

@@ -1,5 +1,103 @@
# @esengine/server
## 4.2.0
### Minor Changes
- [#404](https://github.com/esengine/esengine/pull/404) [`902c0a1`](https://github.com/esengine/esengine/commit/902c0a10749f80bd8f499b44154646379d359704) Thanks [@esengine](https://github.com/esengine)! - feat(server): add HTTP file-based routing support / 添加 HTTP 文件路由支持
New feature that allows organizing HTTP routes in separate files, similar to API and message handlers.
新功能:支持将 HTTP 路由组织在独立文件中,类似于 API 和消息处理器的文件路由方式。
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server';
export default defineHttp<{ username: string; password: string }>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body;
res.json({ token: '...', userId: '...' });
}
});
```
Server configuration / 服务器配置:
```typescript
const server = await createServer({
port: 8080,
httpDir: 'src/http', // HTTP routes directory / HTTP 路由目录
httpPrefix: '/api', // Route prefix / 路由前缀
cors: true
});
```
File naming convention / 文件命名规则:
- `login.ts` → POST /api/login
- `users/profile.ts` → POST /api/users/profile
- `users/[id].ts` → POST /api/users/:id (dynamic routes / 动态路由)
- Set `method: 'GET'` in defineHttp for GET requests / 在 defineHttp 中设置 `method: 'GET'` 以处理 GET 请求
Also includes / 还包括:
- `defineHttp<TBody>()` helper for type-safe route definitions / 类型安全的路由定义辅助函数
- Support for merging file routes with inline `http` config / 支持文件路由与内联 `http` 配置合并
- RPC server supports attaching to existing HTTP server via `server` option / RPC 服务器支持通过 `server` 选项附加到现有 HTTP 服务器
### Patch Changes
- Updated dependencies [[`902c0a1`](https://github.com/esengine/esengine/commit/902c0a10749f80bd8f499b44154646379d359704)]:
- @esengine/rpc@1.1.3
## 4.1.0
### Minor Changes
- feat(server): add HTTP file-based routing support
New feature that allows organizing HTTP routes in separate files, similar to API and message handlers:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server';
export default defineHttp<{ username: string; password: string }>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body;
// ... authentication logic
res.json({ token: '...', userId: '...' });
}
});
```
Server configuration:
```typescript
const server = await createServer({
port: 8080,
httpDir: 'src/http', // HTTP routes directory
httpPrefix: '/api', // Route prefix
cors: true
});
```
File naming convention:
- `login.ts` → POST /api/login
- `users/profile.ts` → POST /api/users/profile
- `users/[id].ts` → POST /api/users/:id (dynamic routes)
- Set `method: 'GET'` in defineHttp for GET requests
Also includes:
- `defineHttp<TBody>()` helper function for type-safe route definitions
- Support for merging file routes with inline `http` config
- RPC server now supports attaching to existing HTTP server via `server` option
### Patch Changes
- Updated dependencies []:
- @esengine/rpc@1.1.2
## 4.0.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/server",
"version": "4.0.0",
"version": "4.2.0",
"description": "Game server framework for ESEngine with file-based routing",
"type": "module",
"main": "./dist/index.js",

View File

@@ -4,6 +4,7 @@
*/
import * as path from 'node:path'
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http'
import { serve, type RpcServer } from '@esengine/rpc/server'
import { rpc } from '@esengine/rpc'
import type {
@@ -14,18 +15,23 @@ import type {
MsgContext,
LoadedApiHandler,
LoadedMsgHandler,
LoadedHttpHandler,
} from '../types/index.js'
import { loadApiHandlers, loadMsgHandlers } from '../router/loader.js'
import type { HttpRoutes, HttpHandler } from '../http/types.js'
import { loadApiHandlers, loadMsgHandlers, loadHttpHandlers } from '../router/loader.js'
import { RoomManager, type RoomClass, type Room } from '../room/index.js'
import { createHttpRouter } from '../http/router.js'
/**
* @zh 默认配置
* @en Default configuration
*/
const DEFAULT_CONFIG: Required<Omit<ServerConfig, 'onStart' | 'onConnect' | 'onDisconnect'>> = {
const DEFAULT_CONFIG: Required<Omit<ServerConfig, 'onStart' | 'onConnect' | 'onDisconnect' | 'http' | 'cors' | 'httpDir' | 'httpPrefix'>> & { httpDir: string; httpPrefix: string } = {
port: 3000,
apiDir: 'src/api',
msgDir: 'src/msg',
httpDir: 'src/http',
httpPrefix: '/api',
tickRate: 20,
}
@@ -56,12 +62,53 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir))
const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir))
// 加载 HTTP 文件路由
const httpDir = config.httpDir ?? opts.httpDir
const httpPrefix = config.httpPrefix ?? opts.httpPrefix
const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix)
if (apiHandlers.length > 0) {
console.log(`[Server] Loaded ${apiHandlers.length} API handlers`)
}
if (msgHandlers.length > 0) {
console.log(`[Server] Loaded ${msgHandlers.length} message handlers`)
}
if (httpHandlers.length > 0) {
console.log(`[Server] Loaded ${httpHandlers.length} HTTP handlers`)
}
// 合并 HTTP 路由(文件路由 + 内联路由)
const mergedHttpRoutes: HttpRoutes = {}
// 先添加文件路由
for (const handler of httpHandlers) {
const existingRoute = mergedHttpRoutes[handler.route]
if (existingRoute && typeof existingRoute !== 'function') {
(existingRoute as Record<string, HttpHandler>)[handler.method] = handler.definition.handler
} else {
mergedHttpRoutes[handler.route] = {
[handler.method]: handler.definition.handler,
}
}
}
// 再添加内联路由(覆盖文件路由)
if (config.http) {
for (const [route, handlerOrMethods] of Object.entries(config.http)) {
if (typeof handlerOrMethods === 'function') {
mergedHttpRoutes[route] = handlerOrMethods
} else {
const existing = mergedHttpRoutes[route]
if (existing && typeof existing !== 'function') {
Object.assign(existing, handlerOrMethods)
} else {
mergedHttpRoutes[route] = handlerOrMethods
}
}
}
}
const hasHttpRoutes = Object.keys(mergedHttpRoutes).length > 0
// 动态构建协议
const apiDefs: Record<string, ReturnType<typeof rpc.api>> = {
@@ -90,6 +137,7 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
let currentTick = 0
let tickInterval: ReturnType<typeof setInterval> | null = null
let rpcServer: RpcServer<typeof protocol, Record<string, unknown>> | null = null
let httpServer: HttpServer | null = null
// 房间管理器(立即初始化,以便 define() 可在 start() 前调用)
const roomManager = new RoomManager((conn, type, data) => {
@@ -200,26 +248,68 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
}
}
rpcServer = serve(protocol, {
port: opts.port,
createConnData: () => ({}),
onStart: (p) => {
console.log(`[Server] Started on ws://localhost:${p}`)
opts.onStart?.(p)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
// 玩家断线时自动离开房间
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
// 如果有 HTTP 路由,创建 HTTP 服务器
if (hasHttpRoutes) {
const httpRouter = createHttpRouter(mergedHttpRoutes, config.cors ?? true)
await rpcServer.start()
httpServer = createHttpServer(async (req, res) => {
// 先尝试 HTTP 路由
const handled = await httpRouter(req, res)
if (!handled) {
// 未匹配的请求返回 404
res.statusCode = 404
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ error: 'Not Found' }))
}
})
// 使用 HTTP 服务器创建 RPC
rpcServer = serve(protocol, {
server: httpServer,
createConnData: () => ({}),
onStart: () => {
console.log(`[Server] Started on http://localhost:${opts.port}`)
opts.onStart?.(opts.port)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
await rpcServer.start()
// 启动 HTTP 服务器
await new Promise<void>((resolve) => {
httpServer!.listen(opts.port, () => resolve())
})
} else {
// 仅 WebSocket 模式
rpcServer = serve(protocol, {
port: opts.port,
createConnData: () => ({}),
onStart: (p) => {
console.log(`[Server] Started on ws://localhost:${p}`)
opts.onStart?.(p)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
await rpcServer.start()
}
// 启动 tick 循环
if (opts.tickRate > 0) {
@@ -238,6 +328,15 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
await rpcServer.stop()
rpcServer = null
}
if (httpServer) {
await new Promise<void>((resolve, reject) => {
httpServer!.close((err) => {
if (err) reject(err)
else resolve()
})
})
httpServer = null
}
},
broadcast(name, data) {

View File

@@ -1,9 +1,9 @@
/**
* @zh API 和消息定义助手
* @en API and message definition helpers
* @zh API、消息和 HTTP 定义助手
* @en API, message, and HTTP definition helpers
*/
import type { ApiDefinition, MsgDefinition } from '../types/index.js'
import type { ApiDefinition, MsgDefinition, HttpDefinition } from '../types/index.js'
/**
* @zh 定义 API 处理器
@@ -49,3 +49,33 @@ export function defineMsg<TMsg, TData = Record<string, unknown>>(
): MsgDefinition<TMsg, TData> {
return definition
}
/**
* @zh 定义 HTTP 路由处理器
* @en Define HTTP route handler
*
* @example
* ```typescript
* // src/http/login.ts
* import { defineHttp } from '@esengine/server'
*
* interface LoginBody {
* username: string
* password: string
* }
*
* export default defineHttp<LoginBody>({
* method: 'POST',
* handler(req, res) {
* const { username, password } = req.body
* // ... validate credentials
* res.json({ token: '...', userId: '...' })
* }
* })
* ```
*/
export function defineHttp<TBody = unknown>(
definition: HttpDefinition<TBody>
): HttpDefinition<TBody> {
return definition
}

View File

@@ -0,0 +1,7 @@
/**
* @zh HTTP 模块导出
* @en HTTP module exports
*/
export * from './types.js';
export { createHttpRouter } from './router.js';

View File

@@ -0,0 +1,263 @@
/**
* @zh HTTP 路由器
* @en HTTP Router
*
* @zh 简洁的 HTTP 路由实现,支持与 WebSocket 共用端口
* @en Simple HTTP router implementation, supports sharing port with WebSocket
*/
import type { IncomingMessage, ServerResponse } from 'node:http';
import type {
HttpRequest,
HttpResponse,
HttpHandler,
HttpRoutes,
CorsOptions,
} from './types.js';
/**
* @zh 创建 HTTP 请求对象
* @en Create HTTP request object
*/
async function createRequest(req: IncomingMessage): Promise<HttpRequest> {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
// 解析查询参数
const query: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
// 解析请求体
let body: unknown = null;
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
body = await parseBody(req);
}
// 获取客户端 IP
const ip =
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
req.socket?.remoteAddress ||
'unknown';
return {
raw: req,
method: req.method ?? 'GET',
path: url.pathname,
query,
headers: req.headers as Record<string, string | string[] | undefined>,
body,
ip,
};
}
/**
* @zh 解析请求体
* @en Parse request body
*/
function parseBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
req.on('end', () => {
const rawBody = Buffer.concat(chunks).toString('utf-8');
if (!rawBody) {
resolve(null);
return;
}
const contentType = req.headers['content-type'] ?? '';
if (contentType.includes('application/json')) {
try {
resolve(JSON.parse(rawBody));
} catch {
resolve(rawBody);
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const params = new URLSearchParams(rawBody);
const result: Record<string, string> = {};
params.forEach((value, key) => {
result[key] = value;
});
resolve(result);
} else {
resolve(rawBody);
}
});
req.on('error', () => {
resolve(null);
});
});
}
/**
* @zh 创建 HTTP 响应对象
* @en Create HTTP response object
*/
function createResponse(res: ServerResponse): HttpResponse {
let statusCode = 200;
const response: HttpResponse = {
raw: res,
status(code: number) {
statusCode = code;
return response;
},
header(name: string, value: string) {
res.setHeader(name, value);
return response;
},
json(data: unknown) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.statusCode = statusCode;
res.end(JSON.stringify(data));
},
text(data: string) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.statusCode = statusCode;
res.end(data);
},
error(code: number, message: string) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.statusCode = code;
res.end(JSON.stringify({ error: message }));
},
};
return response;
}
/**
* @zh 应用 CORS 头
* @en Apply CORS headers
*/
function applyCors(res: ServerResponse, req: IncomingMessage, cors: CorsOptions): void {
const origin = req.headers.origin;
// 处理 origin
if (cors.origin === true || cors.origin === '*') {
res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
} else if (typeof cors.origin === 'string') {
res.setHeader('Access-Control-Allow-Origin', cors.origin);
} else if (Array.isArray(cors.origin) && origin && cors.origin.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 允许的方法
if (cors.methods) {
res.setHeader('Access-Control-Allow-Methods', cors.methods.join(', '));
} else {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
}
// 允许的头
if (cors.allowedHeaders) {
res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders.join(', '));
} else {
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
// 凭证
if (cors.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// 缓存
if (cors.maxAge) {
res.setHeader('Access-Control-Max-Age', String(cors.maxAge));
}
}
/**
* @zh 创建 HTTP 路由器
* @en Create HTTP router
*/
export function createHttpRouter(routes: HttpRoutes, cors?: CorsOptions | boolean) {
// 解析路由
const parsedRoutes: Array<{
method: string;
path: string;
handler: HttpHandler;
}> = [];
for (const [path, handlerOrMethods] of Object.entries(routes)) {
if (typeof handlerOrMethods === 'function') {
// 简单形式:路径 -> 处理器(接受所有方法)
parsedRoutes.push({ method: '*', path, handler: handlerOrMethods });
} else {
// 对象形式:路径 -> { GET, POST, ... }
for (const [method, handler] of Object.entries(handlerOrMethods)) {
if (handler !== undefined) {
parsedRoutes.push({ method, path, handler });
}
}
}
}
// 默认 CORS 配置
const corsOptions: CorsOptions | null =
cors === true
? { origin: true, credentials: true }
: cors === false
? null
: cors ?? null;
/**
* @zh 处理 HTTP 请求
* @en Handle HTTP request
*/
return async function handleRequest(
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
const path = url.pathname;
const method = req.method ?? 'GET';
// 应用 CORS
if (corsOptions) {
applyCors(res, req, corsOptions);
// 处理预检请求
if (method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return true;
}
}
// 查找匹配的路由
const route = parsedRoutes.find(
(r) => r.path === path && (r.method === '*' || r.method === method)
);
if (!route) {
return false; // 未找到路由,让其他处理器处理
}
try {
const httpReq = await createRequest(req);
const httpRes = createResponse(res);
await route.handler(httpReq, httpRes);
return true;
} catch (error) {
console.error('[HTTP] Route handler error:', error);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Internal Server Error' }));
return true;
}
};
}

View File

@@ -0,0 +1,161 @@
/**
* @zh HTTP 路由类型定义
* @en HTTP router type definitions
*/
import type { IncomingMessage, ServerResponse } from 'node:http';
/**
* @zh HTTP 请求上下文
* @en HTTP request context
*/
export interface HttpRequest {
/**
* @zh 原始请求对象
* @en Raw request object
*/
raw: IncomingMessage;
/**
* @zh 请求方法
* @en Request method
*/
method: string;
/**
* @zh 请求路径
* @en Request path
*/
path: string;
/**
* @zh 查询参数
* @en Query parameters
*/
query: Record<string, string>;
/**
* @zh 请求头
* @en Request headers
*/
headers: Record<string, string | string[] | undefined>;
/**
* @zh 解析后的 JSON 请求体
* @en Parsed JSON body
*/
body: unknown;
/**
* @zh 客户端 IP
* @en Client IP
*/
ip: string;
}
/**
* @zh HTTP 响应工具
* @en HTTP response utilities
*/
export interface HttpResponse {
/**
* @zh 原始响应对象
* @en Raw response object
*/
raw: ServerResponse;
/**
* @zh 设置状态码
* @en Set status code
*/
status(code: number): HttpResponse;
/**
* @zh 设置响应头
* @en Set response header
*/
header(name: string, value: string): HttpResponse;
/**
* @zh 发送 JSON 响应
* @en Send JSON response
*/
json(data: unknown): void;
/**
* @zh 发送文本响应
* @en Send text response
*/
text(data: string): void;
/**
* @zh 发送错误响应
* @en Send error response
*/
error(code: number, message: string): void;
}
/**
* @zh HTTP 路由处理器
* @en HTTP route handler
*/
export type HttpHandler = (req: HttpRequest, res: HttpResponse) => void | Promise<void>;
/**
* @zh HTTP 路由定义
* @en HTTP route definition
*/
export interface HttpRoute {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | '*';
path: string;
handler: HttpHandler;
}
/**
* @zh HTTP 路由配置
* @en HTTP routes configuration
*/
export type HttpRoutes = Record<string, HttpHandler | {
GET?: HttpHandler;
POST?: HttpHandler;
PUT?: HttpHandler;
DELETE?: HttpHandler;
PATCH?: HttpHandler;
OPTIONS?: HttpHandler;
}>;
/**
* @zh CORS 配置
* @en CORS configuration
*/
export interface CorsOptions {
/**
* @zh 允许的来源
* @en Allowed origins
*/
origin?: string | string[] | boolean;
/**
* @zh 允许的方法
* @en Allowed methods
*/
methods?: string[];
/**
* @zh 允许的请求头
* @en Allowed headers
*/
allowedHeaders?: string[];
/**
* @zh 是否允许携带凭证
* @en Allow credentials
*/
credentials?: boolean;
/**
* @zh 预检请求缓存时间(秒)
* @en Preflight cache max age
*/
maxAge?: number;
}

View File

@@ -30,7 +30,7 @@
export { createServer } from './core/server.js'
// Helpers
export { defineApi, defineMsg } from './helpers/define.js'
export { defineApi, defineMsg, defineHttp } from './helpers/define.js'
// Room System
export { Room, type RoomOptions } from './room/Room.js'
@@ -46,7 +46,19 @@ export type {
MsgContext,
ApiDefinition,
MsgDefinition,
HttpDefinition,
HttpMethod,
} from './types/index.js'
// HTTP
export { createHttpRouter } from './http/router.js'
export type {
HttpRequest,
HttpResponse,
HttpHandler,
HttpRoutes,
CorsOptions,
} from './http/types.js'
// Re-export useful types from @esengine/rpc
export { RpcError, ErrorCode } from '@esengine/rpc'

View File

@@ -6,7 +6,15 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import type { ApiDefinition, MsgDefinition, LoadedApiHandler, LoadedMsgHandler } from '../types/index.js'
import type {
ApiDefinition,
MsgDefinition,
HttpDefinition,
LoadedApiHandler,
LoadedMsgHandler,
LoadedHttpHandler,
HttpMethod,
} from '../types/index.js'
/**
* @zh 将文件名转换为 API/消息名称
@@ -110,3 +118,106 @@ export async function loadMsgHandlers(msgDir: string): Promise<LoadedMsgHandler[
return handlers
}
/**
* @zh 递归扫描目录获取所有处理器文件
* @en Recursively scan directory for all handler files
*/
function scanDirectoryRecursive(dir: string, baseDir: string = dir): Array<{ filePath: string; relativePath: string }> {
if (!fs.existsSync(dir)) {
return []
}
const files: Array<{ filePath: string; relativePath: string }> = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...scanDirectoryRecursive(fullPath, baseDir))
} else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
if (entry.name.startsWith('_') || entry.name.startsWith('index.')) {
continue
}
const relativePath = path.relative(baseDir, fullPath)
files.push({ filePath: fullPath, relativePath })
}
}
return files
}
/**
* @zh 将文件路径转换为路由路径
* @en Convert file path to route path
*
* @example
* 'login.ts' -> '/login'
* 'users/profile.ts' -> '/users/profile'
* 'users/[id].ts' -> '/users/:id'
*/
function filePathToRoute(relativePath: string, prefix: string): string {
let route = relativePath
.replace(/\.(ts|js|mts|mjs)$/, '')
.replace(/\\/g, '/')
.replace(/\[([^\]]+)\]/g, ':$1')
if (!route.startsWith('/')) {
route = '/' + route
}
const fullRoute = prefix.endsWith('/')
? prefix.slice(0, -1) + route
: prefix + route
return fullRoute
}
/**
* @zh 加载 HTTP 路由处理器
* @en Load HTTP route handlers
*
* @example
* ```typescript
* // Directory structure:
* // src/http/
* // login.ts -> POST /api/login
* // register.ts -> POST /api/register
* // users/
* // [id].ts -> GET /api/users/:id
*
* const handlers = await loadHttpHandlers('src/http', '/api')
* ```
*/
export async function loadHttpHandlers(
httpDir: string,
prefix: string = '/api'
): Promise<LoadedHttpHandler[]> {
const files = scanDirectoryRecursive(httpDir)
const handlers: LoadedHttpHandler[] = []
for (const { filePath, relativePath } of files) {
try {
const fileUrl = pathToFileURL(filePath).href
const module = await import(fileUrl)
const definition = module.default as HttpDefinition<unknown>
if (definition && typeof definition.handler === 'function') {
const route = filePathToRoute(relativePath, prefix)
const method: HttpMethod = definition.method ?? 'POST'
handlers.push({
route,
method,
path: filePath,
definition,
})
}
} catch (err) {
console.warn(`[Server] Failed to load HTTP handler: ${filePath}`, err)
}
}
return handlers
}

View File

@@ -4,6 +4,7 @@
*/
import type { Connection, ProtocolDef } from '@esengine/rpc'
import type { HttpRoutes, CorsOptions, HttpRequest, HttpResponse } from '../http/types.js'
// ============================================================================
// Server Config
@@ -35,6 +36,29 @@ export interface ServerConfig {
*/
msgDir?: string
/**
* @zh HTTP 路由目录路径
* @en HTTP routes directory path
* @default 'src/http'
*
* @zh 文件命名规则:
* - `login.ts` → POST /api/login
* - `users/[id].ts` → /api/users/:id
* - `health.ts` (method: 'GET') → GET /api/health
* @en File naming convention:
* - `login.ts` → POST /api/login
* - `users/[id].ts` → /api/users/:id
* - `health.ts` (method: 'GET') → GET /api/health
*/
httpDir?: string
/**
* @zh HTTP 路由前缀
* @en HTTP routes prefix
* @default '/api'
*/
httpPrefix?: string
/**
* @zh 游戏 Tick 速率 (每秒)
* @en Game tick rate (per second)
@@ -42,6 +66,19 @@ export interface ServerConfig {
*/
tickRate?: number
/**
* @zh HTTP 路由配置(内联定义,与 httpDir 文件路由合并)
* @en HTTP routes configuration (inline definition, merged with httpDir file routes)
*/
http?: HttpRoutes
/**
* @zh CORS 配置
* @en CORS configuration
* @default true
*/
cors?: CorsOptions | boolean
/**
* @zh 服务器启动回调
* @en Server start callback
@@ -232,3 +269,80 @@ export interface LoadedMsgHandler {
path: string
definition: MsgDefinition<any, any>
}
// ============================================================================
// HTTP Definition
// ============================================================================
/**
* @zh HTTP 请求方法
* @en HTTP request method
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
/**
* @zh HTTP 定义选项
* @en HTTP definition options
*
* @example
* ```typescript
* // src/http/login.ts
* import { defineHttp } from '@esengine/server'
*
* export default defineHttp({
* method: 'POST',
* handler: async (req, res) => {
* const { username, password } = req.body
* // ... authentication logic
* res.json({ token: '...', userId: '...' })
* }
* })
* ```
*/
export interface HttpDefinition<TBody = unknown> {
/**
* @zh 请求方法
* @en Request method
* @default 'POST'
*/
method?: HttpMethod
/**
* @zh 处理函数
* @en Handler function
*/
handler: (
req: HttpRequest & { body: TBody },
res: HttpResponse
) => void | Promise<void>
}
/**
* @zh 已加载的 HTTP 处理器
* @en Loaded HTTP handler
*/
export interface LoadedHttpHandler {
/**
* @zh 路由路径(如 /api/login
* @en Route path (e.g., /api/login)
*/
route: string
/**
* @zh 请求方法
* @en Request method
*/
method: HttpMethod
/**
* @zh 源文件路径
* @en Source file path
*/
path: string
/**
* @zh 处理器定义
* @en Handler definition
*/
definition: HttpDefinition<any>
}

View File

@@ -1,5 +1,79 @@
# @esengine/transaction
## 2.1.1
### Patch Changes
- Updated dependencies [[`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20)]:
- @esengine/database-drivers@1.1.1
## 2.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - feat: add database layer architecture
Added new database packages with layered architecture:
**@esengine/database-drivers (Layer 1)**
- MongoDB connection with pool management, auto-reconnect, events
- Redis connection with auto-reconnect, key prefix
- Type-safe `IMongoCollection<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` with CRUD, pagination, soft delete
- `UserRepository` with registration, authentication, role management
- Password hashing utilities using scrypt
- Query operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$regex`
**@esengine/transaction**
- Refactored `MongoStorage` to use shared connection from `@esengine/database-drivers`
- Removed factory pattern in favor of shared connection (breaking change)
- Simplified API: `createMongoStorage(connection, options?)`
Example usage:
```typescript
import { createMongoConnection } from '@esengine/database-drivers';
import { UserRepository } from '@esengine/database';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// Create shared connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// Use for database operations
const userRepo = new UserRepository(mongo);
await userRepo.register({ username: 'john', password: '123456' });
// Use for transactions (same connection)
const storage = createMongoStorage(mongo);
const txManager = new TransactionManager({ storage });
```
### Patch Changes
- Updated dependencies [[`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa)]:
- @esengine/database-drivers@1.1.0
## 2.0.7
### Patch Changes
- Updated dependencies [[`902c0a1`](https://github.com/esengine/esengine/commit/902c0a10749f80bd8f499b44154646379d359704)]:
- @esengine/server@4.2.0
## 2.0.6
### Patch Changes
- Updated dependencies []:
- @esengine/server@4.1.0
## 2.0.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/transaction",
"version": "2.0.5",
"version": "2.1.1",
"description": "Game transaction system with distributed support | 游戏事务系统,支持分布式事务",
"type": "module",
"main": "./dist/index.js",
@@ -25,7 +25,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@esengine/server": "workspace:*"
"@esengine/database-drivers": "workspace:*"
},
"peerDependencies": {
"ioredis": "^5.3.0",

View File

@@ -88,9 +88,7 @@ export {
export {
MongoStorage,
createMongoStorage,
type MongoStorageConfig,
type MongoDb,
type MongoCollection
type MongoStorageConfig
} from './storage/MongoStorage.js';
// =============================================================================

View File

@@ -2,10 +2,11 @@
* @zh MongoDB 存储实现
* @en MongoDB storage implementation
*
* @zh 支持持久化事务日志和查询
* @en Supports persistent transaction logs and queries
* @zh 基于共享连接的事务存储,使用 @esengine/database-drivers 提供的连接
* @en Transaction storage based on shared connection from @esengine/database-drivers
*/
import type { IMongoConnection, IMongoCollection } from '@esengine/database-drivers';
import type {
ITransactionStorage,
TransactionLog,
@@ -13,43 +14,9 @@ import type {
OperationLog
} from '../core/types.js';
/**
* @zh MongoDB Collection 接口
* @en MongoDB Collection interface
*/
export interface MongoCollection<T> {
findOne(filter: object): Promise<T | null>
find(filter: object): {
toArray(): Promise<T[]>
}
insertOne(doc: T): Promise<{ insertedId: unknown }>
updateOne(filter: object, update: object): Promise<{ modifiedCount: number }>
deleteOne(filter: object): Promise<{ deletedCount: number }>
createIndex(spec: object, options?: object): Promise<string>
}
/**
* @zh MongoDB 数据库接口
* @en MongoDB database interface
*/
export interface MongoDb {
collection<T = unknown>(name: string): MongoCollection<T>
}
/**
* @zh MongoDB 客户端接口
* @en MongoDB client interface
*/
export interface MongoClient {
db(name?: string): MongoDb
close(): Promise<void>
}
/**
* @zh MongoDB 连接工厂
* @en MongoDB connection factory
*/
export type MongoClientFactory = () => MongoClient | Promise<MongoClient>
// =============================================================================
// 配置类型 | Configuration Types
// =============================================================================
/**
* @zh MongoDB 存储配置
@@ -57,29 +24,10 @@ export type MongoClientFactory = () => MongoClient | Promise<MongoClient>
*/
export interface MongoStorageConfig {
/**
* @zh MongoDB 客户端工厂(惰性连接)
* @en MongoDB client factory (lazy connection)
*
* @example
* ```typescript
* import { MongoClient } from 'mongodb'
* const storage = new MongoStorage({
* factory: async () => {
* const client = new MongoClient('mongodb://localhost:27017')
* await client.connect()
* return client
* },
* database: 'game'
* })
* ```
* @zh MongoDB 连接(来自 @esengine/database-drivers
* @en MongoDB connection (from @esengine/database-drivers)
*/
factory: MongoClientFactory
/**
* @zh 数据库名称
* @en Database name
*/
database: string
connection: IMongoConnection
/**
* @zh 事务日志集合名称
@@ -100,6 +48,10 @@ export interface MongoStorageConfig {
lockCollection?: string
}
// =============================================================================
// 内部类型 | Internal Types
// =============================================================================
interface LockDocument {
_id: string
token: string
@@ -112,50 +64,40 @@ interface DataDocument {
expireAt?: Date
}
// =============================================================================
// 实现 | Implementation
// =============================================================================
/**
* @zh MongoDB 存储
* @en MongoDB storage
*
* @zh 基于 MongoDB 的事务存储,支持持久化、复杂查询和惰性连接
* @en MongoDB-based transaction storage with persistence, complex queries and lazy connection
* @zh 基于 MongoDB 的事务存储,使用 @esengine/database-drivers 的共享连接
* @en MongoDB-based transaction storage using shared connection from @esengine/database-drivers
*
* @example
* ```typescript
* import { MongoClient } from 'mongodb'
* import { createMongoConnection } from '@esengine/database-drivers'
* import { MongoStorage } from '@esengine/transaction'
*
* // 创建存储(惰性连接,首次操作时才连接)
* const storage = new MongoStorage({
* factory: async () => {
* const client = new MongoClient('mongodb://localhost:27017')
* await client.connect()
* return client
* },
* const mongo = createMongoConnection({
* uri: 'mongodb://localhost:27017',
* database: 'game'
* })
* await mongo.connect()
*
* await storage.ensureIndexes()
*
* // 使用后手动关闭
* await storage.close()
*
* // 或使用 await using 自动关闭 (TypeScript 5.2+)
* await using storage = new MongoStorage({ ... })
* // 作用域结束时自动关闭
* const storage = new MongoStorage({ connection: mongo })
* ```
*/
export class MongoStorage implements ITransactionStorage {
private _client: MongoClient | null = null;
private _db: MongoDb | null = null;
private _factory: MongoClientFactory;
private _database: string;
private _transactionCollection: string;
private _dataCollection: string;
private _lockCollection: string;
private readonly _connection: IMongoConnection;
private readonly _transactionCollection: string;
private readonly _dataCollection: string;
private readonly _lockCollection: string;
private _closed: boolean = false;
constructor(config: MongoStorageConfig) {
this._factory = config.factory;
this._database = config.database;
this._connection = config.connection;
this._transactionCollection = config.transactionCollection ?? 'transactions';
this._dataCollection = config.dataCollection ?? 'transaction_data';
this._lockCollection = config.lockCollection ?? 'transaction_locks';
@@ -166,36 +108,30 @@ export class MongoStorage implements ITransactionStorage {
// =========================================================================
/**
* @zh 获取数据库实例(惰性连接)
* @en Get database instance (lazy connection)
* @zh 获取集合
* @en Get collection
*/
private async _getDb(): Promise<MongoDb> {
private _getCollection<T extends object>(name: string): IMongoCollection<T> {
if (this._closed) {
throw new Error('MongoStorage is closed');
}
if (!this._db) {
this._client = await this._factory();
this._db = this._client.db(this._database);
if (!this._connection.isConnected()) {
throw new Error('MongoDB connection is not connected');
}
return this._db;
return this._connection.collection<T>(name);
}
/**
* @zh 关闭存储连接
* @en Close storage connection
* @zh 关闭存储
* @en Close storage
*
* @zh 不会关闭共享连接,只标记存储为已关闭
* @en Does not close shared connection, only marks storage as closed
*/
async close(): Promise<void> {
if (this._closed) return;
this._closed = true;
if (this._client) {
await this._client.close();
this._client = null;
this._db = null;
}
}
/**
@@ -211,16 +147,15 @@ export class MongoStorage implements ITransactionStorage {
* @en Ensure indexes exist
*/
async ensureIndexes(): Promise<void> {
const db = await this._getDb();
const txColl = db.collection<TransactionLog>(this._transactionCollection);
const txColl = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
await txColl.createIndex({ state: 1 });
await txColl.createIndex({ 'metadata.serverId': 1 });
await txColl.createIndex({ createdAt: 1 });
const lockColl = db.collection<LockDocument>(this._lockCollection);
const lockColl = this._getCollection<LockDocument>(this._lockCollection);
await lockColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
const dataColl = db.collection<DataDocument>(this._dataCollection);
const dataColl = this._getCollection<DataDocument>(this._dataCollection);
await dataColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
}
@@ -229,19 +164,14 @@ export class MongoStorage implements ITransactionStorage {
// =========================================================================
async acquireLock(key: string, ttl: number): Promise<string | null> {
const db = await this._getDb();
const coll = db.collection<LockDocument>(this._lockCollection);
const coll = this._getCollection<LockDocument>(this._lockCollection);
const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`;
const expireAt = new Date(Date.now() + ttl);
try {
await coll.insertOne({
_id: key,
token,
expireAt
});
await coll.insertOne({ _id: key, token, expireAt } as LockDocument);
return token;
} catch (error) {
} catch {
const existing = await coll.findOne({ _id: key });
if (existing && existing.expireAt < new Date()) {
const result = await coll.updateOne(
@@ -257,8 +187,7 @@ export class MongoStorage implements ITransactionStorage {
}
async releaseLock(key: string, token: string): Promise<boolean> {
const db = await this._getDb();
const coll = db.collection<LockDocument>(this._lockCollection);
const coll = this._getCollection<LockDocument>(this._lockCollection);
const result = await coll.deleteOne({ _id: key, token });
return result.deletedCount > 0;
}
@@ -268,8 +197,7 @@ export class MongoStorage implements ITransactionStorage {
// =========================================================================
async saveTransaction(tx: TransactionLog): Promise<void> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const existing = await coll.findOne({ _id: tx.id });
if (existing) {
@@ -278,13 +206,12 @@ export class MongoStorage implements ITransactionStorage {
{ $set: { ...tx, _id: tx.id } }
);
} else {
await coll.insertOne({ ...tx, _id: tx.id });
await coll.insertOne({ ...tx, _id: tx.id } as TransactionLog & { _id: string });
}
}
async getTransaction(id: string): Promise<TransactionLog | null> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const doc = await coll.findOne({ _id: id });
if (!doc) return null;
@@ -294,8 +221,7 @@ export class MongoStorage implements ITransactionStorage {
}
async updateTransactionState(id: string, state: TransactionState): Promise<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
await coll.updateOne(
{ _id: id },
{ $set: { state, updatedAt: Date.now() } }
@@ -308,8 +234,7 @@ export class MongoStorage implements ITransactionStorage {
state: OperationLog['state'],
error?: string
): Promise<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const update: Record<string, unknown> = {
[`operations.${operationIndex}.state`]: state,
@@ -333,8 +258,7 @@ export class MongoStorage implements ITransactionStorage {
}
async getPendingTransactions(serverId?: string): Promise<TransactionLog[]> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const filter: Record<string, unknown> = {
state: { $in: ['pending', 'executing'] }
@@ -344,13 +268,12 @@ export class MongoStorage implements ITransactionStorage {
filter['metadata.serverId'] = serverId;
}
const docs = await coll.find(filter).toArray();
const docs = await coll.find(filter);
return docs.map(({ _id, ...tx }) => tx as TransactionLog);
}
async deleteTransaction(id: string): Promise<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
await coll.deleteOne({ _id: id });
}
@@ -359,8 +282,7 @@ export class MongoStorage implements ITransactionStorage {
// =========================================================================
async get<T>(key: string): Promise<T | null> {
const db = await this._getDb();
const coll = db.collection<DataDocument>(this._dataCollection);
const coll = this._getCollection<DataDocument>(this._dataCollection);
const doc = await coll.findOne({ _id: key });
if (!doc) return null;
@@ -374,13 +296,9 @@ export class MongoStorage implements ITransactionStorage {
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const db = await this._getDb();
const coll = db.collection<DataDocument>(this._dataCollection);
const coll = this._getCollection<DataDocument>(this._dataCollection);
const doc: DataDocument = {
_id: key,
value
};
const doc: DataDocument = { _id: key, value };
if (ttl) {
doc.expireAt = new Date(Date.now() + ttl);
@@ -395,8 +313,7 @@ export class MongoStorage implements ITransactionStorage {
}
async delete(key: string): Promise<boolean> {
const db = await this._getDb();
const coll = db.collection(this._dataCollection);
const coll = this._getCollection<DataDocument>(this._dataCollection);
const result = await coll.deleteOne({ _id: key });
return result.deletedCount > 0;
}
@@ -405,7 +322,24 @@ export class MongoStorage implements ITransactionStorage {
/**
* @zh 创建 MongoDB 存储
* @en Create MongoDB storage
*
* @example
* ```typescript
* import { createMongoConnection } from '@esengine/database-drivers'
* import { createMongoStorage } from '@esengine/transaction'
*
* const mongo = createMongoConnection({
* uri: 'mongodb://localhost:27017',
* database: 'game'
* })
* await mongo.connect()
*
* const storage = createMongoStorage(mongo)
* ```
*/
export function createMongoStorage(config: MongoStorageConfig): MongoStorage {
return new MongoStorage(config);
export function createMongoStorage(
connection: IMongoConnection,
options?: Omit<MongoStorageConfig, 'connection'>
): MongoStorage {
return new MongoStorage({ connection, ...options });
}

View File

@@ -5,4 +5,4 @@
export { MemoryStorage, createMemoryStorage, type MemoryStorageConfig } from './MemoryStorage.js';
export { RedisStorage, createRedisStorage, type RedisStorageConfig, type RedisClient } from './RedisStorage.js';
export { MongoStorage, createMongoStorage, type MongoStorageConfig, type MongoDb, type MongoCollection } from './MongoStorage.js';
export { MongoStorage, createMongoStorage, type MongoStorageConfig } from './MongoStorage.js';

View File

@@ -14,7 +14,7 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageRoot = join(__dirname, '..');
const rapierRoot = join(packageRoot, '..', '..', 'thirdparty', 'rapier.js');
const rapierRoot = join(packageRoot, '..', '..', '..', 'thirdparty', 'rapier.js');
const srcTsDir = join(rapierRoot, 'src.ts');
const src2dDir = join(rapierRoot, 'rapier-compat', 'src2d');
const outputDir = join(packageRoot, 'src');

View File

@@ -1,12 +1,60 @@
// @ts-ignore
import wasmBase64 from "../pkg/rapier_wasm2d_bg.wasm";
/**
* RAPIER initialization module with dynamic WASM loading support.
* RAPIER 初始化模块,支持动态 WASM 加载。
*/
import wasmInit from "../pkg/rapier_wasm2d";
import base64 from "base64-js";
/**
* Input types for WASM initialization.
* WASM 初始化的输入类型。
*/
export type InitInput =
| RequestInfo // URL string or Request object
| URL // URL object
| Response // Fetch Response object
| BufferSource // ArrayBuffer or TypedArray
| WebAssembly.Module; // Pre-compiled module
let initialized = false;
/**
* Initializes RAPIER.
* Has to be called and awaited before using any library methods.
*
* 初始化 RAPIER。
* 必须在使用任何库方法之前调用并等待。
*
* @param input - WASM source (required). Can be URL, Response, ArrayBuffer, etc.
* WASM 源(必需)。可以是 URL、Response、ArrayBuffer 等。
*
* @example
* // Load from URL | 从 URL 加载
* await RAPIER.init('wasm/rapier_wasm2d_bg.wasm');
*
* @example
* // Load from fetch response | 从 fetch 响应加载
* const response = await fetch('wasm/rapier_wasm2d_bg.wasm');
* await RAPIER.init(response);
*
* @example
* // Load from ArrayBuffer | 从 ArrayBuffer 加载
* const buffer = await fetch('wasm/rapier_wasm2d_bg.wasm').then(r => r.arrayBuffer());
* await RAPIER.init(buffer);
*/
export async function init() {
await wasmInit(base64.toByteArray(wasmBase64 as unknown as string).buffer);
export async function init(input?: InitInput): Promise<void> {
if (initialized) {
return;
}
await wasmInit(input);
initialized = true;
}
/**
* Check if RAPIER is already initialized.
* 检查 RAPIER 是否已初始化。
*/
export function isInitialized(): boolean {
return initialized;
}

View File

@@ -6,7 +6,7 @@ export default defineConfig({
dts: true,
sourcemap: true,
clean: true,
external: ["../pkg/rapier_wasm2d.js"],
external: [/\.\.\/pkg\/rapier_wasm2d/],
loader: {
".wasm": "base64",
},

44
pnpm-lock.yaml generated
View File

@@ -1506,6 +1506,46 @@ importers:
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
publishDirectory: dist
packages/framework/database:
dependencies:
'@esengine/database-drivers':
specifier: workspace:*
version: link:../database-drivers
devDependencies:
'@types/node':
specifier: ^20.0.0
version: 20.19.27
rimraf:
specifier: ^5.0.0
version: 5.0.10
tsup:
specifier: ^8.0.0
version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@20.19.27))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.8.0
version: 5.9.3
packages/framework/database-drivers:
devDependencies:
'@types/node':
specifier: ^20.0.0
version: 20.19.27
ioredis:
specifier: ^5.3.0
version: 5.8.2
mongodb:
specifier: ^6.12.0
version: 6.21.0(socks@2.8.7)
rimraf:
specifier: ^5.0.0
version: 5.0.10
tsup:
specifier: ^8.0.0
version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@20.19.27))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.8.0
version: 5.9.3
packages/framework/fsm:
dependencies:
tslib:
@@ -1786,9 +1826,9 @@ importers:
packages/framework/transaction:
dependencies:
'@esengine/server':
'@esengine/database-drivers':
specifier: workspace:*
version: link:../server
version: link:../database-drivers
ioredis:
specifier: ^5.3.0
version: 5.8.2

146
scripts/build-rapier2d.mjs Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* Build Rapier2D WASM artifacts
* 构建 Rapier2D WASM 产物
*
* This script automates the entire Rapier2D WASM build process:
* 此脚本自动化整个 Rapier2D WASM 构建流程:
*
* 1. Prepare Rust project from thirdparty/rapier.js
* 2. Build WASM using wasm-pack
* 3. Copy artifacts to packages/physics/rapier2d/pkg
* 4. Generate TypeScript source code
*/
import { execSync, spawn } from 'child_process';
import { existsSync, cpSync, rmSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const rapierJsDir = join(rootDir, 'thirdparty', 'rapier.js');
const rapier2dBuildDir = join(rapierJsDir, 'builds', 'rapier2d');
const rapier2dPkgSrc = join(rapier2dBuildDir, 'pkg');
const rapier2dPkgDest = join(rootDir, 'packages', 'physics', 'rapier2d', 'pkg');
/**
* Run a command and stream output
*/
function runCommand(command, cwd, description) {
console.log(`\n📦 ${description}...`);
console.log(` Running: ${command}`);
console.log(` In: ${cwd}\n`);
try {
execSync(command, {
cwd,
stdio: 'inherit',
shell: true
});
return true;
} catch (error) {
console.error(`❌ Failed: ${description}`);
return false;
}
}
/**
* Main build function
*/
async function build() {
console.log('🚀 Building Rapier2D WASM...\n');
// Check if rapier.js exists
if (!existsSync(rapierJsDir)) {
console.error('❌ Error: thirdparty/rapier.js not found!');
console.error(' Please clone it first:');
console.error(' git clone https://github.com/esengine/rapier.js.git thirdparty/rapier.js');
process.exit(1);
}
// Check if Rust/Cargo is installed
try {
execSync('cargo --version', { stdio: 'pipe' });
} catch {
console.error('❌ Error: Rust/Cargo not found!');
console.error(' Please install Rust: https://rustup.rs/');
process.exit(1);
}
// Check if wasm-pack is installed
try {
execSync('wasm-pack --version', { stdio: 'pipe' });
} catch {
console.error('❌ Error: wasm-pack not found!');
console.error(' Please install it: cargo install wasm-pack');
process.exit(1);
}
// Step 1: Prepare Rust project
if (!runCommand(
'cargo run -p prepare_builds -- -d dim2 -f non-deterministic',
rapierJsDir,
'Step 1/4: Preparing Rust project'
)) {
process.exit(1);
}
// Step 2: Install npm dependencies for rapier2d build
if (!runCommand(
'npm install',
rapier2dBuildDir,
'Step 2/4: Installing npm dependencies'
)) {
process.exit(1);
}
// Step 3: Build WASM
if (!runCommand(
'npm run build',
rapier2dBuildDir,
'Step 3/4: Building WASM'
)) {
process.exit(1);
}
// Step 4: Copy pkg to packages/physics/rapier2d/pkg
console.log('\n📦 Step 4/4: Copying WASM artifacts...');
console.log(` From: ${rapier2dPkgSrc}`);
console.log(` To: ${rapier2dPkgDest}\n`);
if (!existsSync(rapier2dPkgSrc)) {
console.error('❌ Error: Build output not found at', rapier2dPkgSrc);
process.exit(1);
}
// Remove old pkg if exists
if (existsSync(rapier2dPkgDest)) {
rmSync(rapier2dPkgDest, { recursive: true });
}
// Copy new pkg
cpSync(rapier2dPkgSrc, rapier2dPkgDest, { recursive: true });
console.log(' ✅ Copied successfully!\n');
// Step 5: Generate TypeScript source
if (!runCommand(
'pnpm --filter @esengine/rapier2d gen:src',
rootDir,
'Bonus: Generating TypeScript source'
)) {
console.warn('⚠️ Warning: Failed to generate TypeScript source.');
console.warn(' You can run it manually: pnpm --filter @esengine/rapier2d gen:src');
}
console.log('\n✅ Rapier2D WASM build completed successfully!');
console.log('\nNext steps:');
console.log(' 1. Run: pnpm build:editor');
console.log(' 2. Start editor: cd packages/editor/editor-app && pnpm tauri:dev\n');
}
build().catch(error => {
console.error('❌ Build failed:', error);
process.exit(1);
});