@crossdelta/cloudevents

中文文档
0.10.1发布于 2026/10/25MITTypeScriptESM + CJS近 30 天下载 4041

CloudEvents toolkit for TypeScript - Zod validation, handler discovery, NATS JetStream & Core

原始使用文档
文档目录23 节
  1. Quick Start
  2. Why use this?
  3. Core Concepts
  4. Event Type vs. Event Data
  5. Handlers
  6. Publishing
  7. Stream Setup
  8. Consuming
  9. Configuration
  10. Environment Variables
  11. Optional: Google Cloud Pub/Sub
  12. Stream Options
  13. Consumer Options
  14. Advanced Features
  15. Basic Contract with Channel
  16. Channel Configuration
  17. Multiple Events, Same Stream
  18. Infrastructure Materialization
  19. Development vs. Production
  20. Backward Compatibility
  21. API
  22. Distributed Tracing
  23. License

@crossdelta/cloudevents

Type-safe event-driven microservices with NATS and Zod validation, using the CloudEvents specification.

                         NATS JetStream
                        ┌──────────────┐
  ┌──────────────┐      │              │      ┌──────────────┐
  │    Service   │      │   Stream:    │      │   Service    │
  │   (publish)  │─────▶│   ORDERS     │─────▶│  (consume)   │
  └──────────────┘      │              │      └──────────────┘
                        └──────────────┘
        │                                            │
        │  publish(...)                              │  handleEvent(...)
        ▼                                            ▼
  ┌──────────────┐                            ┌──────────────┐
  │ { orderId,   │                            │ Zod schema   │
  │   total }    │                            │ validates    │
  └──────────────┘                            └──────────────┘
bun add @crossdelta/cloudevents zod@4

Prerequisites: A running NATS server with JetStream enabled.

Note: Requires Zod v4 for full TypeScript support.

Quick Start

Note: Services never create streams. Streams must exist before services consume from them:

  • Development: Use NATS CLI or Platform SDK (pf dev auto-creates from contracts)
  • Production: Use infrastructure-as-code (Pulumi, Terraform) or the Stream Setup function below

1. Create an event handler (src/events/order-created.handler.ts):

import { handleEvent } from '@crossdelta/cloudevents'
import { z } from 'zod'

// Export schema for mock generation
export const OrderCreatedSchema = z.object({
  orderId: z.string(),
  total: z.number(),
})

// Export type for use in use-cases
export type OrderCreatedEvent = z.infer<typeof OrderCreatedSchema>

export default handleEvent(
  {
    schema: OrderCreatedSchema,
    type: 'order.created',
  },
  async (data) => {
    console.log(`New order: ${data.orderId}, total: ${data.total}`)
  },
)

2. Start consuming:

import { consumeJetStreams } from '@crossdelta/cloudevents'

consumeJetStreams({
  streams: ['ORDERS'],
  consumer: 'my-service',
  discover: './src/events/**/*.handler.ts',
})

3. Publish from another service:

import { publish } from '@crossdelta/cloudevents'

await publish('order.created', { orderId: 'ord_123', total: 99.99 })
// String API auto-pluralizes the domain token: subject → 'orders.created'
// Use createContract({ type, channel, schema }) for explicit subject control (no pluralization)

That's it. Handlers are auto-discovered, validated with Zod, and messages persist in JetStream.

Stream creation is handled by your development environment or infrastructure — not by services:


Why use this?

ProblemSolution
Messages lost on restartJetStream persists messages
Scattered handler registrationAuto-discovery via glob patterns
Runtime type errorsZod validation with TypeScript inference
Poison messages crash servicesDLQ quarantines invalid messages

Core Concepts

Event Type vs. Event Data

Important distinction:

  • Event Type (order.created): Lives in the CloudEvent envelope (ce.type). Used for routing and handler matching.
  • Event Data ({ orderId, total }): The actual payload. Does not include the type.
const Schema = z.object({
  orderId: z.string(), 
})

export default handleEvent(
  {
    schema: Schema,
    type: 'order.created',
  },
  async (data) => { ... }
)

Handlers

Drop a *.handler.ts file anywhere — it's auto-registered:

// src/events/user-signup.handler.ts
import { z } from 'zod'

const UserSignupSchema = z.object({ 
  email: z.email(),
  name: z.string(),
})

// Export type for use in use-cases
export type UserSignupEvent = z.infer<typeof UserSignupSchema>

export default handleEvent(
  {
    schema: UserSignupSchema,
    type: 'user.signup',
  },
  async (data) => {
    await sendWelcomeEmail(data.email)
  },
)

Publishing

await publish('order.created', orderData)

Stream Setup

For infrastructure use only - Services should never call this function.

Create streams during infrastructure setup (Pulumi, deployment scripts):

import { ensureJetStreams } from '@crossdelta/cloudevents'

await ensureJetStreams({
  streams: [
    {
      stream: 'ORDERS',
      subjects: ['order.>'],  // singular — matches contract subjects (order.created, order.completed, …)
      config: {
        maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days retention
        replicas: 3,                       // For HA
      },
    },
  ],
})

Consuming

import { consumeJetStreams } from '@crossdelta/cloudevents'

// JetStream (recommended) — persistent, retries, exactly-once
consumeJetStreams({
  streams: ['ORDERS'],
  consumer: 'billing',
  discover: './src/events/**/*.handler.ts',
})

// Core NATS — fire-and-forget, simpler
await consumeNatsEvents({
  subject: 'notifications.*',
  consumerName: 'my-service',
  discover: './src/events/**/*.handler.ts',
})

// NATS Core Request-Reply — synchronous validation feedback
// Consumer returns result, publisher awaits it
import { request } from '@crossdelta/cloudevents'
const result = await request('call.received', payload)
// result: { accepted: true, data: { ... } } | { accepted: false, error: '...' }

Configuration

Environment Variables

NATS_URL=nats://localhost:4222
NATS_USER=myuser        # optional
NATS_PASSWORD=mypass    # optional

Optional: Google Cloud Pub/Sub

To use Google Cloud Pub/Sub instead of NATS, install the optional dependency:

bun add @google-cloud/pubsub
# or
npm install @google-cloud/pubsub

Then use the publishToPubSub function:

import { publishToPubSub } from '@crossdelta/cloudevents'

await publishToPubSub('order.created', { orderId: 'ord_123', total: 99.99 })

Note: @google-cloud/pubsub is not bundled with this library to keep the package size small. Install it only if you need Google Cloud Pub/Sub support.

Stream Options

await ensureJetStreams({
  streams: [
    {
      // Required
      stream: 'ORDERS',
      subjects: ['order.>'],  // singular — matches contract subjects

      // Optional
      config: {
        maxAge: 7 * 24 * 60 * 60 * 1000,  // Message retention (ms)
        maxBytes: 1024 * 1024 * 1024,       // Reserved at creation, not on write
        replicas: 1,                        // Replication factor
        storage: 'file',                    // 'file' or 'memory'
        retention: 'limits',                // 'limits', 'interest', or 'workqueue'
      },
    },
  ],
  // Optional
  servers: 'nats://localhost:4222',
})

Consumer Options

consumeJetStreams({
  // Required
  streams: ['ORDERS'],
  consumer: 'my-service',
  discover: './src/events/**/*.handler.ts',

  // Optional
  servers: 'nats://localhost:4222',
  filterSubjects: ['order.created'],  // Filter specific subjects
  maxDeliver: 5,                       // Retry attempts
  ackWait: 30_000,                     // Timeout per attempt (ms)
  quarantineTopic: 'dlq',              // For poison messages
})

Advanced Features

Channel Metadata (Infrastructure Integration)

Contracts can include channel metadata to define which NATS JetStream stream they belong to. This enables infrastructure-as-code workflows where streams are materialized from contracts.

Basic Contract with Channel

import { createContract } from '@crossdelta/cloudevents'
import { z } from 'zod'

export const OrdersCreatedContract = createContract({
  type: 'order.created',
  channel: {
    stream: 'ORDERS',
    // subject defaults to 'order.created' if omitted
  },
  schema: z.object({
    orderId: z.string(),
    total: z.number(),
  }),
})

Channel Configuration

interface ChannelConfig {
  stream: string    // JetStream stream name (e.g., 'ORDERS')
  subject?: string  // NATS subject (optional override)
}

Subject Routing:

  • Without subject: defaults to type — order.created stays order.created
  • With subject: uses that exact string

Note: publish('order.x', data) (string API) auto-pluralizes → orders.x. Contracts do not. Configure JetStream streams with singular subjects (order.>) to match contract events.

// subject = 'order.created' (from type)
createContract({ type: 'order.created', channel: { stream: 'ORDERS' } })

// subject = 'orders.v1.created' (explicit override)
createContract({ type: 'order.created', channel: { stream: 'ORDERS', subject: 'orders.v1.created' } })

Multiple Events, Same Stream

// order.created → ORDERS stream
export const OrdersCreatedContract = createContract({
  type: 'order.created',
  channel: { stream: 'ORDERS' },
  schema: OrderCreatedSchema,
})

// order.updated → ORDERS stream (same stream!)
export const OrdersUpdatedContract = createContract({
  type: 'order.updated',
  channel: { stream: 'ORDERS' },
  schema: OrderUpdatedSchema,
})

Result: Both events share the ORDERS stream with subjects order.created and order.updated.

Infrastructure Materialization

Channel metadata is conceptual - it defines routing, not infrastructure policy.

Contracts define:

  • Event type (order.created)
  • Stream routing (ORDERS)
  • Subject mapping (order.created)

Infrastructure defines:

  • Retention (7 days, 14 days, etc.)
  • Storage (disk, memory)
  • Replicas (1, 3, etc.)
  • Limits (max message size, etc.)

Example Infrastructure (Pulumi):

import { collectStreamDefinitions } from './helpers/streams'
import { ensureJetStreams } from '@crossdelta/cloudevents'

// Collect from contracts
const streams = collectStreamDefinitions()
// [{ stream: 'ORDERS', subjects: ['order.created', 'order.updated'] }]

// Materialize with policies
await ensureJetStreams({
  streams: streams.map(({ stream, subjects }) => ({
    stream,
    subjects,
    config: {
      maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days
      replicas: 3,                       // High availability
      storage: 'file',                   // Persistent
    },
  })),
})

Development vs. Production

EnvironmentStreamsHowRetention
DevelopmentEphemeralAuto-created by servicesNone (memory)
ProductionPersistentMaterialized via infrastructureExplicit (7d, 14d, etc.)

Key Principle: Contracts define what and where, infrastructure defines how and how long.

Backward Compatibility

Channel metadata is optional - contracts without channel work as before:

// Legacy contract (still works)
export const LegacyContract = createContract({
  type: 'legacy.event',
  schema: LegacySchema,
  // No channel - handler still works
})
Shared Contracts

When multiple services consume the same event, use createContract to create shared event contracts:

// packages/contracts/src/events/orders/created.ts
import { createContract } from '@crossdelta/cloudevents'
import { z } from 'zod'

export const OrderCreatedSchema = z.object({
  orderId: z.string(),
  total: z.number(),
})

export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>

export const OrdersCreatedContract = createContract({
  type: 'order.created',
  schema: OrderCreatedSchema,
})

Then import and use in handlers:

import { handleEvent } from '@crossdelta/cloudevents'
import { OrdersCreatedContract } from '@my-org/contracts'

export default handleEvent(
  OrdersCreatedContract,
  async (data) => {
    // data is typed as OrderCreatedData
    console.log(data.orderId)
  },
)

Benefits:

  • Single source of truth for event schemas
  • No schema duplication
  • Type safety across services
Multi-Tenancy

Filter events by tenant:

export default handleEvent({
  type: 'order.created',
  schema: OrderSchema,
  tenantId: 'tenant-a',  // Only process tenant-a events
}, async (data) => { ... })
Wildcard Handler (Catch-All)

Use type: '*' to create a handler that processes all events, regardless of type. Useful for audit logging, metrics collection, or event replay.

import { handleEvent } from '@crossdelta/cloudevents'
import { z } from 'zod'

export default handleEvent(
  {
    type: '*',
    schema: z.record(z.string(), z.unknown()),
  },
  async (data, context) => {
    await persistAuditEvent({
      eventType: context.eventType,
      source: context.source,
      data,
    })
  },
)

Routing behavior:

  • Specific handlers (type: 'order.created') always take priority
  • The wildcard handler runs only when no specific handler matches
  • Only one wildcard handler per consumer is recommended
  • Works with both JetStream and NATS Core consumers
Custom Matching

Add custom filter logic:

export default handleEvent({
  type: 'order.created',
  schema: OrderSchema,
  match: (event) => event.data.total > 100,  // Only high-value orders
}, async (data) => { ... })
Idempotency

Deduplication is built-in. For distributed systems, provide a Redis store:

const redisStore = {
  has: (id) => redis.exists(`idem:${id}`),
  add: (id, ttl) => redis.set(`idem:${id}`, '1', 'PX', ttl),
}

await consumeJetStreams({
  streams: ['ORDERS'],
  consumer: 'my-service',
  discover: './src/events/**/*.handler.ts',
  idempotencyStore: redisStore,
})
Dead Letter Queue

Invalid messages are quarantined, not lost:

await consumeJetStreams({
  streams: ['ORDERS'],
  consumer: 'my-service',
  discover: './src/events/**/*.handler.ts',
  quarantineTopic: 'events.quarantine',  // For malformed messages
  errorTopic: 'events.errors',            // For handler errors
})

Messages that fail validation go to quarantineTopic, messages that crash handlers go to errorTopic.

HTTP Ingestion (Hono)
import { Hono } from 'hono'
import { cloudEvents } from '@crossdelta/cloudevents'

const app = new Hono()
app.use('/events', cloudEvents({ discover: 'src/events/**/*.handler.ts' }))

API

FunctionPurpose
handleEvent(options, handler)Create a handler (supports type: '*' wildcard)
createContract(options)Create shared event contract
ensureJetStreams(options)Create/update JetStream streams
consumeJetStreams(options)Consume from multiple streams
consumeNatsEvents(options)Consume fire-and-forget
publish(type, data)Publish event to JetStream (fire-and-forget)
request(type, data, opts?)Send command via NATS Core and await synchronous reply
isNatsConnected()Check if NATS publisher connection is live (for health checks)
isConsumerConnected()Check if any NATS consumer connection is live (for readiness probes)
configureNatsPublisher(config)Set connection options for the publisher singleton
closeConnection()Drain and close the NATS connection (for CLI tools)

Distributed Tracing

When @crossdelta/telemetry is installed (optional peer dependency), trace context is propagated automatically across NATS:

  • Publish: The current W3C traceparent is injected as a CloudEvent extension field.
  • Consume: The traceparent extension is extracted and restored in AsyncLocalStorage before the handler runs.

This enables end-to-end distributed traces in Grafana:

Storefront → API Gateway → NATS → Orders Service → NATS → Audit Service
                └── all spans share the same traceId ──┘

No configuration required. If @crossdelta/telemetry is not installed, CloudEvents work normally without trace propagation.

License

MIT © crossdelta

版本与发布记录

@crossdelta/cloudevents 共发布 71 个版本,首次发布于 2025/12/03;过去 12 个月发布了 71 个版本。

版本发布日期距今
0.10.1latest2026/10/2516 天前
0.10.02026/09/082 个月前
0.9.02026/09/072 个月前
0.8.42026/07/074 个月前
0.8.32026/07/024 个月前
0.8.22026/04/256 个月前

关于 @crossdelta/cloudevents 的常见问题

@crossdelta/cloudevents 是什么?

CloudEvents toolkit for TypeScript - Zod validation, handler discovery, NATS JetStream & Core

如何安装 @crossdelta/cloudevents?

在项目目录运行 npm install @crossdelta/cloudevents,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(^4.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 cloudevents,可以用 npx cloudevents 直接运行。

@crossdelta/cloudevents 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crossdelta/cloudevents 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@crossdelta/cloudevents 可以商用吗?

@crossdelta/cloudevents 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crossdelta/cloudevents 的下载量有多少?

根据 npm 官方下载统计,@crossdelta/cloudevents 在 2026/10/11 – 2026/11/09 这 30 天内被下载 4,041 次,日均约 135 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crossdelta/cloudevents 还在维护吗?

最新版本 0.10.1 发布于 2026/10/25(16 天前);至今共发布 71 个版本,其中过去 12 个月发布了 71 个;首次发布于 2025/12/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。