Skip to content

Commit 0170f06

Browse files
committed
feat: update README documentation for clarity and structure
1 parent 7a369c9 commit 0170f06

2 files changed

Lines changed: 54 additions & 492 deletions

File tree

README.md

Lines changed: 27 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,31 @@ Build MQTT applications with an Elysia-like API: compose broker adapters, ordere
66

77
mqttkit does not reimplement the MQTT protocol. A broker such as Aedes owns CONNECT, SUBSCRIBE, PUBLISH, QoS, retain, sessions, persistence, and MQTT-over-WebSocket. mqttkit adds an application framework layer on top.
88

9+
Full documentation: **<https://mqttkit.keyp.dev>** ([简体中文](https://mqttkit.keyp.dev/zh/))
10+
911
## Features
1012

1113
- Ordered `use()` middleware for auth, logging, audit, validation, and interception.
12-
- `router().topic()` declares MQTT topic routes, publish policy, and subscribe policy.
13-
- `ctx.params` extracts topic params such as `devices/:uid/events`.
14-
- `ctx.body` carries the validated payload — any [Standard Schema](https://standardschema.dev/) validator (zod, valibot, arktype, …) works, with full type inference. Use `@mqttkit/typebox` / `@mqttkit/zod` for raw TypeBox or for attaching JSON Schema.
15-
- `ctx.services` injects Kafka, Redis, database, audit, or domain services.
16-
- `app.request()` does MQTT 5 RPC over `responseTopic` + `correlationData`; `ctx.reply()` answers on the device side.
17-
- `app.onError()` (and per-route `onError`) capture validation, handler, policy, middleware and publish failures with a structured `phase`.
18-
- `app.on()` observes broker lifecycle events such as client, publish, subscribe, ack, and errors.
19-
- `app.publish()` lets services, workers, and consumers push messages to MQTT clients through the broker.
20-
- `@mqttkit/core/testing` ships an in-memory `TestBroker` so apps can be unit-tested without spawning aedes.
21-
- `@mqttkit/aedes` provides TCP MQTT and MQTT-over-WebSocket support through Aedes (with MQTT 5 properties forwarded for RPC).
22-
- `@mqttkit/asyncapi` generates AsyncAPI 3.0 documentation from routes and serves a browsable docs page.
23-
- Built for Bun and TypeScript.
14+
- `router().topic()` declares MQTT topic routes with publish / subscribe policies.
15+
- Topic params (`devices/:uid/events`), payload validation via any [Standard Schema](https://standardschema.dev/) validator, and injected services on `ctx`.
16+
- MQTT 5 RPC with `app.request()` and `ctx.reply()`.
17+
- `app.on()` observes broker lifecycle events; `app.publish()` lets workers push messages.
18+
- Adapters: `@mqttkit/aedes` (TCP + WebSocket) and `@mqttkit/asyncapi` (AsyncAPI 3.0 docs).
19+
- In-memory `TestBroker` for unit tests. Built for Bun and TypeScript.
2420

2521
## Installation
2622

2723
```bash
2824
bun add @mqttkit/core @mqttkit/aedes aedes
2925
```
3026

31-
Install only `@mqttkit/core` if you only need the core types, router, or a custom broker adapter.
32-
3327
## Quick Start
3428

3529
```ts
3630
import { aedes } from '@mqttkit/aedes'
3731
import { MqttApp, router } from '@mqttkit/core'
3832

39-
type Services = {
40-
audit: {
41-
log(event: string, fields: Record<string, unknown>): Promise<void>
42-
}
43-
}
44-
45-
const app = new MqttApp<{ principal?: { uid: string }; services: Services }>()
46-
.decorate('audit', {
47-
async log(event, fields) {
48-
console.log(event, fields)
49-
},
50-
})
33+
const app = new MqttApp<{ principal?: { uid: string } }>()
5134
.use(
5235
aedes({
5336
tcp: { port: 1883 },
@@ -58,15 +41,8 @@ const app = new MqttApp<{ principal?: { uid: string }; services: Services }>()
5841
},
5942
}),
6043
)
61-
.use(async (ctx, next) => {
62-
await ctx.services.audit.log('mqtt.message', {
63-
clientId: ctx.clientId,
64-
topic: ctx.topic,
65-
})
66-
await next()
67-
})
6844
.use(
69-
router<{ principal?: { uid: string }; services: Services }>()
45+
router<{ principal?: { uid: string } }>()
7046
.topic('devices/:uid/events', {
7147
publish: ({ params, principal }) => params.uid === principal?.uid,
7248
async onMessage(ctx) {
@@ -81,241 +57,46 @@ const app = new MqttApp<{ principal?: { uid: string }; services: Services }>()
8157
await app.listen()
8258
```
8359

84-
Clients connect with standard MQTT TCP or MQTT-over-WebSocket:
85-
86-
```ts
87-
import mqtt from 'mqtt'
88-
89-
const client = mqtt.connect('ws://localhost:8888/mqtt', {
90-
username: 'demo',
91-
})
92-
93-
client.subscribe('server/demo/echo')
94-
client.publish('devices/demo/events', JSON.stringify({ temperature: 22 }), { qos: 0 })
95-
```
96-
97-
## Router
98-
99-
MQTT has topics, not HTTP methods. mqttkit uses `topic(pattern, config)` to declare a route, publish policy, subscribe policy, and optional inbound message handler.
100-
101-
```ts
102-
router()
103-
.topic('devices/:uid/events', {
104-
publish: ({ params, principal }) => params.uid === principal?.uid,
105-
subscribe: false,
106-
async onMessage(ctx) {
107-
console.log(ctx.params.uid, ctx.payload.toString())
108-
},
109-
})
110-
.topic('server/:uid/commands', {
111-
publish: false,
112-
subscribe: ({ params, principal }) => params.uid === principal?.uid,
113-
})
114-
```
115-
116-
Default policies:
117-
118-
- A topic with `onMessage` defaults to `publish: true` and `subscribe: false`.
119-
- A topic without `onMessage` defaults to `publish: false` and `subscribe: true`.
120-
121-
## Middleware
122-
123-
`use()` runs in registration order. If middleware does not call `next()`, the remaining middleware and route handler do not run.
124-
125-
```ts
126-
const app = new MqttApp()
127-
.use(async (ctx, next) => {
128-
console.log('[mqtt]', ctx.clientId, ctx.topic)
129-
await next()
130-
})
131-
.use(
132-
router()
133-
.use(async (ctx, next) => {
134-
if (ctx.params.uid === 'blocked') return
135-
await next()
136-
})
137-
.topic('devices/:uid/events', {
138-
onMessage(ctx) {
139-
console.log(ctx.payload.toString())
140-
},
141-
}),
142-
)
143-
```
144-
145-
Pipeline:
146-
147-
```text
148-
app middleware -> route middleware -> onMessage
149-
```
150-
151-
## Events
152-
153-
`app.on()` receives broker lifecycle events. The Aedes adapter currently forwards:
154-
155-
```ts
156-
import type { MqttEventName } from '@mqttkit/core'
157-
158-
const eventNames: MqttEventName[] = [
159-
'client',
160-
'clientReady',
161-
'clientDisconnect',
162-
'keepaliveTimeout',
163-
'clientError',
164-
'connectionError',
165-
'connackSent',
166-
'ping',
167-
'publish',
168-
'ack',
169-
'subscribe',
170-
'unsubscribe',
171-
]
172-
173-
for (const eventName of eventNames) {
174-
app.on(eventName, (event) => {
175-
console.log(event.type, event.clientId, event.topic)
176-
})
177-
}
178-
```
179-
180-
ACK is produced by the broker according to MQTT packet lifecycle. mqttkit does not define a custom JSON ack.
181-
182-
## Service Push
183-
184-
Any external service, timer, worker, queue consumer, or Kafka consumer can call `app.publish()` to send messages into the broker and let the broker deliver them to subscribed MQTT clients.
185-
186-
```text
187-
external service / worker / consumer
188-
-> app.publish(topic, payload, options)
189-
-> @mqttkit/aedes adapter
190-
-> Aedes broker
191-
-> subscribed MQTT clients
192-
```
193-
194-
Declare a server-owned topic that clients may subscribe to but may not publish to:
195-
196-
```ts
197-
const app = new MqttApp()
198-
.use(aedes({ tcp: { port: 1886 } }))
199-
.use(
200-
router().topic('users/:uid/notifications', {
201-
subscribe: true,
202-
publish: false,
203-
}),
204-
)
205-
206-
billing.onInvoicePaid(async (event) => {
207-
await app.publish(`users/${event.uid}/notifications`, event.payload, { qos: 1 })
208-
})
209-
```
210-
211-
## Kafka Bridge
60+
## Schema Validation
21261

213-
Kafka, databases, and other services are injected with `decorate()`. MQTT -> Kafka happens inside `onMessage`; Kafka -> MQTT happens when a consumer calls `app.publish()`.
62+
`topic({ schema })` accepts any [Standard Schema](https://standardschema.dev/) validator (zod, valibot, arktype, …). The validated payload is exposed on `ctx.body` with full type inference.
21463

21564
```ts
216-
type Kafka = {
217-
produce(topic: string, value: Buffer, key: string): Promise<void>
218-
onCommands(handler: (message: { key: string; value: Buffer }) => Promise<void>): void
219-
}
65+
import { z } from 'zod'
22066

221-
const app = new MqttApp<{ services: { kafka: Kafka } }>()
222-
.decorate('kafka', kafka)
223-
.use(aedes({ tcp: { port: 1883 } }))
224-
.use(
225-
router<{ services: { kafka: Kafka } }>()
226-
.topic('devices/:uid/events', {
227-
async onMessage(ctx) {
228-
await ctx.services.kafka.produce('device.events', ctx.payload, ctx.params.uid)
229-
},
230-
})
231-
.topic('server/:uid/commands'),
232-
)
233-
234-
kafka.onCommands(async (message) => {
235-
await app.publish(`server/${message.key}/commands`, message.value, { qos: 1 })
236-
})
237-
```
238-
239-
The client only needs a normal MQTT subscription:
240-
241-
```ts
242-
const client = mqtt.connect('mqtt://localhost:1885')
243-
client.subscribe('server/demo/commands', { qos: 1 })
244-
client.on('message', (topic, payload) => {
245-
console.log(topic, payload.toString())
67+
router().topic('devices/:uid/events', {
68+
schema: { body: z.object({ temperature: z.number() }) },
69+
async onMessage(ctx) {
70+
ctx.body.temperature // typed as number
71+
},
24672
})
24773
```
24874

249-
## Aedes Integration
75+
Use [`@mqttkit/typebox`](https://mqttkit.keyp.dev/schema) for raw TypeBox or [`@mqttkit/zod`](https://mqttkit.keyp.dev/schema) to attach a JSON Schema so `@mqttkit/asyncapi` can emit the full payload.
25076

251-
`@mqttkit/aedes` supports:
77+
See the [Getting Started guide](https://mqttkit.keyp.dev/getting-started) for routers, middleware, events, RPC, Kafka bridging, and more.
25278

253-
- Creating an Aedes instance for mqttkit.
254-
- Using an externally created Aedes instance.
255-
- Starting a TCP MQTT server.
256-
- Starting a standard MQTT-over-WebSocket server.
257-
- Passing Aedes `persistence` through.
258-
- Forwarding Aedes lifecycle events to `app.on()`.
259-
- Returning `principal` from `authenticate` and injecting it into policies and handler context.
260-
- Delegating publish / subscribe authorization to mqttkit route policies.
261-
262-
```ts
263-
import { aedes } from '@mqttkit/aedes'
264-
265-
new MqttApp()
266-
.use(
267-
aedes({
268-
tcp: { port: 1883 },
269-
ws: { port: 8888, path: '/mqtt' },
270-
authenticate({ username }) {
271-
if (!username) return false
272-
return { uid: username }
273-
},
274-
}),
275-
)
276-
```
79+
## Packages
27780

278-
Use Aedes persistence adapters for offline queues, retain, QoS sessions, and durable storage. mqttkit only owns the application layer.
81+
- `@mqttkit/core`: application, router, middleware, context, schema validation, RPC, event types, broker adapter interface, and `@mqttkit/core/testing` in-memory broker.
82+
- `@mqttkit/aedes`: Aedes adapter for TCP MQTT and MQTT-over-WebSocket (forwards MQTT 5 properties for RPC).
83+
- `@mqttkit/asyncapi`: AsyncAPI 3.0 generator and HTTP plugin for browsable docs.
84+
- `@mqttkit/typebox`: TypeBox schema provider — register once, then pass raw `Type.X(...)` schemas directly.
85+
- `@mqttkit/zod`: zod helper that attaches a JSON Schema representation so AsyncAPI documents the full payload.
27986

28087
## Examples
28188

282-
- `examples/aedes-basic`: TCP broker, authentication, middleware, topic routes, and server-side publish.
283-
- `examples/aedes-ws`: standard MQTT-over-WebSocket; mqtt.js connects to `ws://localhost:8888/mqtt`.
284-
- `examples/events`: broker lifecycle events.
285-
- `examples/service-push`: external service calls `app.publish()`, then Aedes delivers to subscribed MQTT clients.
286-
- `examples/kafka-bridge`: MQTT messages go to Kafka, and Kafka consumer messages are forwarded to MQTT clients.
287-
- `examples/schema-validation`: payload schemas with zod, TypeBox, and coexistence between the two.
288-
- `examples/rpc`: MQTT 5 RPC round-trip with `app.request()` and `ctx.reply()`.
289-
- `examples/asyncapi-docs`: AsyncAPI documentation served from `@mqttkit/asyncapi` (standalone HTTP). `dev:zod` variant shows zod + `jsonify` in the doc.
290-
- `examples/asyncapi-elysia`: share a single Elysia port for both AsyncAPI docs and MQTT-over-WebSocket (aedes ws attached to the same `http.Server`).
291-
292-
Run an example:
89+
Runnable examples under [`examples/`](./examples) cover the basic TCP / WebSocket broker, lifecycle events, service push, Kafka bridge, schema validation, MQTT 5 RPC, and AsyncAPI docs (standalone HTTP or shared Elysia port).
29390

29491
```bash
29592
bun install
29693
bun run --cwd examples/aedes-basic dev
29794
```
29895

299-
## Documentation
300-
301-
- [Getting Started](docs/getting-started.md) / [简体中文](docs/zh/getting-started.md)
302-
- [Aedes adapter](docs/aedes.md) / [简体中文](docs/zh/aedes.md)
303-
- [Events](docs/events.md) / [简体中文](docs/zh/events.md)
304-
- [Service Push](docs/service-push.md) / [简体中文](docs/zh/service-push.md)
305-
- [Kafka bridge](docs/kafka-bridge.md) / [简体中文](docs/zh/kafka-bridge.md)
306-
30796
## Development
30897

30998
```bash
31099
bun run test
311100
bun run typecheck
312101
bun run build
313102
```
314-
315-
## Packages
316-
317-
- `@mqttkit/core`: core application, router, middleware, context, schema validation, RPC, event types, broker adapter interface, and `@mqttkit/core/testing` in-memory broker.
318-
- `@mqttkit/aedes`: Aedes adapter for TCP MQTT and MQTT-over-WebSocket (forwards MQTT 5 properties for RPC).
319-
- `@mqttkit/asyncapi`: AsyncAPI 3.0 generator and HTTP plugin for browsable docs.
320-
- `@mqttkit/typebox`: TypeBox schema provider — register once, then pass raw `Type.X(...)` schemas directly.
321-
- `@mqttkit/zod`: zod helper that attaches a JSON Schema representation so AsyncAPI documents the full payload.

0 commit comments

Comments
 (0)