@das3mical/adonis-mercure
Version:
A simple AdonisJS provider to interact with Mercure hub
255 lines (176 loc) • 6.68 kB
Markdown
<p align="center">
<img src="https://raw.githubusercontent.com/mdsiha/adonis-mercure/main/assets/logo.svg" width="100" alt="adonis-mercure" />
</p>
<h1 align="center">@das3mical/adonis-mercure</h1>
<p align="center">
<a href="https://www.npmjs.com/package/@das3mical/adonis-mercure"><img src="https://img.shields.io/npm/dm/@das3mical/adonis-mercure.svg?style=flat-square" alt="Downloads"></a>
<a href="https://www.npmjs.com/package/@das3mical/adonis-mercure"><img src="https://img.shields.io/npm/v/@das3mical/adonis-mercure.svg?style=flat-square" alt="Version"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/npm/l/@das3mical/adonis-mercure.svg?style=flat-square" alt="License"></a>
</p>
<p align="center">
AdonisJS v6 package to publish real-time updates via a <a href="https://mercure.rocks">Mercure Hub</a> (SSE)
</p>
## Table of Contents
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [Publish an update](#publish-an-update)
- [Multiple topics](#multiple-topics)
- [Private updates](#private-updates)
- [SSE options](#sse-options-id-type-retry)
- [Generate a subscriber token](#generate-a-subscriber-token)
- [Health check](#health-check)
- [Testing](#testing)
- [API Reference](#api-reference)
- [License](#license)
## Requirements
- AdonisJS **v6**
- Node.js **>= 20.6.0**
- A running [Mercure Hub](https://mercure.rocks/docs/hub/install)
## Installation
```bash
npm install @das3mical/adonis-mercure
node ace configure @das3mical/adonis-mercure
```
## Configuration
After running `ace configure`, a `config/mercure.ts` file is created and your `.env` is updated automatically.
```ts
// config/mercure.ts
import env from '#start/env'
import { defineConfig } from '@das3mical/adonis-mercure'
export default defineConfig({
endpoint: env.get('MERCURE_ENDPOINT'),
adminToken: env.get('MERCURE_ADMIN_JWT'),
jwt: {
alg: 'HS256',
secret: env.get('MERCURE_JWT_SECRET'),
},
})
```
Set the following env variables in your `.env`:
```env
MERCURE_ENDPOINT=http://localhost:3000/.well-known/mercure
MERCURE_ADMIN_JWT=<your-admin-jwt>
MERCURE_JWT_SECRET=<your-jwt-secret>
```
> **Note:** The `adminToken` must be a JWT signed with your hub's secret and a `"publish": ["*"]` claim in the `mercure` field. See the [Mercure auth docs](https://mercure.rocks/docs/hub/auth) for details.
## Usage
Import the service anywhere in your app:
```ts
import mercure from '@das3mical/adonis-mercure/services/main'
```
### Publish an update
```ts
await mercure.send('/orders/42', { status: 'shipped' })
```
### Multiple topics
```ts
await mercure.send(['/orders/42', '/notifications/user/1'], { status: 'shipped' })
```
### Private updates
Private updates are only delivered to authenticated subscribers who hold a valid token for that topic.
```ts
// shorthand (backward compatible)
await mercure.send('/orders/42', { status: 'shipped' }, true)
// options object
await mercure.send('/orders/42', { status: 'shipped' }, { private: true })
```
### SSE options (id, type, retry)
```ts
await mercure.send(
'/orders/42',
{ status: 'shipped' },
{
id: 'msg-001', // event ID — enables reconnection recovery
type: 'order.shipped', // event type
retry: 5000, // client reconnect delay in ms
private: true,
}
)
```
### Generate a subscriber token
Use this to create JWT tokens for your frontend clients so they can subscribe to topics, including private ones.
```ts
// typed shorthand
const token = await mercure.generateSubscribeToken(['/orders/42'])
// or low-level
const token = await mercure.generate({ subscribe: ['/orders/42'] })
```
Pass the token to your frontend:
```ts
const url = new URL('http://localhost:3000/.well-known/mercure')
url.searchParams.append('topic', '/orders/42')
const eventSource = new EventSource(url.toString(), {
headers: { Authorization: `Bearer ${token}` },
})
```
### Health check
```ts
const isReachable = await mercure.ping() // true | false
```
## Testing
`FakeMercure` lets you test your application code without a real Mercure Hub.
**Swap the container binding in your test setup:**
```ts
import { FakeMercure } from '@das3mical/adonis-mercure'
// before your test
app.container.swap('mercure', () => new FakeMercure())
// after your test
app.container.restore('mercure')
```
**Assert on what was sent:**
```ts
const fake = (await app.container.make('mercure')) as FakeMercure
// assert a topic received a message
fake.assertSent('/orders/42')
// assert a topic received a specific payload
fake.assertSent('/orders/42', { status: 'shipped' })
// assert a topic was never sent to
fake.assertNotSent('/admin/secret')
// assert nothing was sent at all
fake.assertNothingSent()
// inspect all recorded messages
const messages = fake.getSent()
// reset between tests
fake.clear()
```
## API Reference
### `send(topics, data?, options?)`
Publishes an update to the Mercure Hub.
| Parameter | Type | Default | Description |
| --------- | ------------------------- | ------- | ------------------------------------------------------ |
| `topics` | `string \| string[]` | — | Topic(s) to publish to |
| `data` | `Record<string, unknown>` | `{}` | Payload — serialized as JSON |
| `options` | `boolean \| SendOptions` | `false` | `true` for private (legacy), or a `SendOptions` object |
**`SendOptions`**
| Property | Type | Description |
| --------- | --------- | ---------------------------------------------- |
| `private` | `boolean` | Restrict delivery to authenticated subscribers |
| `id` | `string` | SSE event ID (enables reconnection recovery) |
| `type` | `string` | SSE event type |
| `retry` | `number` | Client reconnect delay in milliseconds |
Throws `MercurePublishError` if the hub returns a non-2xx response.
### `generateSubscribeToken(topics)`
Generates a JWT with `{ subscribe: topics }` for a frontend client.
```ts
const token = await mercure.generateSubscribeToken(['/chat/1', '/notifications/me'])
```
### `generate(payload)`
Low-level JWT generation. Wraps `payload` under the `mercure` claim.
```ts
const token = await mercure.generate({ subscribe: ['/chat/1'], publish: ['/chat/1'] })
```
### `ping()`
Returns `true` if the hub is reachable, `false` on network error.
## License
MIT — [Michael DAŞ](https://github.com/mdsiha)