<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)
