@mdsiha369/adonis-mercure 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +274 -0
- package/build/bin/test.d.ts +1 -0
- package/build/bin/test.js +8 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +34 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +14 -0
- package/build/providers/mercure_provider.d.ts +7 -0
- package/build/providers/mercure_provider.js +18 -0
- package/build/services/mercure.d.ts +3 -0
- package/build/services/mercure.js +6 -0
- package/build/src/define_config.d.ts +2 -0
- package/build/src/define_config.js +11 -0
- package/build/src/errors/config_error.d.ts +4 -0
- package/build/src/errors/config_error.js +7 -0
- package/build/src/errors/mercure_error.d.ts +3 -0
- package/build/src/errors/mercure_error.js +6 -0
- package/build/src/errors/publish_error.d.ts +6 -0
- package/build/src/errors/publish_error.js +11 -0
- package/build/src/errors/timeout_error.d.ts +5 -0
- package/build/src/errors/timeout_error.js +9 -0
- package/build/src/fake_mercure.d.ts +18 -0
- package/build/src/fake_mercure.js +52 -0
- package/build/src/mercure.d.ts +9 -0
- package/build/src/mercure.js +42 -0
- package/build/src/publisher.d.ts +6 -0
- package/build/src/publisher.js +54 -0
- package/build/src/token_generator.d.ts +7 -0
- package/build/src/token_generator.js +24 -0
- package/build/src/types/extended.d.ts +6 -0
- package/build/src/types/extended.js +1 -0
- package/build/src/types/main.d.ts +24 -0
- package/build/src/types/main.js +1 -0
- package/build/stubs/config/mercure.stub +15 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/tests/fake_mercure.spec.d.ts +1 -0
- package/build/tests/fake_mercure.spec.js +76 -0
- package/build/tests/mercure.spec.d.ts +1 -0
- package/build/tests/mercure.spec.js +84 -0
- package/build/tests/publisher.spec.d.ts +1 -0
- package/build/tests/publisher.spec.js +101 -0
- package/build/tests/token_generator.spec.d.ts +1 -0
- package/build/tests/token_generator.spec.js +29 -0
- package/configure.ts +41 -0
- package/index.ts +14 -0
- package/package.json +115 -0
- package/providers/mercure_provider.ts +23 -0
- package/src/define_config.ts +18 -0
- package/src/errors/config_error.ts +8 -0
- package/src/errors/mercure_error.ts +6 -0
- package/src/errors/publish_error.ts +14 -0
- package/src/errors/timeout_error.ts +8 -0
- package/src/fake_mercure.ts +74 -0
- package/src/mercure.ts +54 -0
- package/src/publisher.ts +62 -0
- package/src/token_generator.ts +29 -0
- package/src/types/extended.ts +7 -0
- package/src/types/main.ts +31 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mdsiha/adonis-mercure/main/assets/logo.svg" width="100" alt="adonis-mercure" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@mdsiha369/adonis-mercure</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://github.com/mdsiha/adonis-mercure/actions/workflows/test.yml"><img src="https://github.com/mdsiha/adonis-mercure/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@mdsiha369/adonis-mercure"><img src="https://img.shields.io/npm/dm/@mdsiha369/adonis-mercure.svg?style=flat-square" alt="Downloads"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@mdsiha369/adonis-mercure"><img src="https://img.shields.io/npm/v/@mdsiha369/adonis-mercure.svg?style=flat-square" alt="Version"></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/npm/l/@mdsiha369/adonis-mercure.svg?style=flat-square" alt="License"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
AdonisJS v6 package to publish real-time updates via a <a href="https://mercure.rocks">Mercure Hub</a> (SSE)
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Requirements](#requirements)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Configuration](#configuration)
|
|
25
|
+
- [Usage](#usage)
|
|
26
|
+
- [Publish an update](#publish-an-update)
|
|
27
|
+
- [Multiple topics](#multiple-topics)
|
|
28
|
+
- [Private updates](#private-updates)
|
|
29
|
+
- [SSE options](#sse-options-id-type-retry)
|
|
30
|
+
- [Generate a subscriber token](#generate-a-subscriber-token)
|
|
31
|
+
- [Health check](#health-check)
|
|
32
|
+
- [Testing](#testing)
|
|
33
|
+
- [API Reference](#api-reference)
|
|
34
|
+
- [Roadmap](#roadmap)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- AdonisJS **v6**
|
|
42
|
+
- Node.js **>= 20.6.0**
|
|
43
|
+
- A running [Mercure Hub](https://mercure.rocks/docs/hub/install)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @mdsiha369/adonis-mercure
|
|
49
|
+
node ace configure @mdsiha369/adonis-mercure
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
After running `ace configure`, a `config/mercure.ts` file is created and your `.env` is updated automatically.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// config/mercure.ts
|
|
58
|
+
import env from '#start/env'
|
|
59
|
+
import { defineConfig } from '@mdsiha369/adonis-mercure'
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
endpoint: env.get('MERCURE_ENDPOINT'),
|
|
63
|
+
adminToken: env.get('MERCURE_ADMIN_JWT'),
|
|
64
|
+
jwt: {
|
|
65
|
+
alg: 'HS256',
|
|
66
|
+
secret: env.get('MERCURE_JWT_SECRET'),
|
|
67
|
+
},
|
|
68
|
+
// optional — default: 5000ms, set to 0 to disable
|
|
69
|
+
http: {
|
|
70
|
+
timeout: 5000,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Set the following env variables in your `.env`:
|
|
76
|
+
|
|
77
|
+
```env
|
|
78
|
+
MERCURE_ENDPOINT=http://localhost:3000/.well-known/mercure
|
|
79
|
+
MERCURE_ADMIN_JWT=<your-admin-jwt>
|
|
80
|
+
MERCURE_JWT_SECRET=<your-jwt-secret>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> **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.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
Import the service anywhere in your app:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import mercure from '@mdsiha369/adonis-mercure/services/main'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Publish an update
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await mercure.send('/orders/42', { status: 'shipped' })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Multiple topics
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
await mercure.send(['/orders/42', '/notifications/user/1'], { status: 'shipped' })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Private updates
|
|
108
|
+
|
|
109
|
+
Private updates are only delivered to authenticated subscribers who hold a valid token for that topic.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// shorthand (backward compatible)
|
|
113
|
+
await mercure.send('/orders/42', { status: 'shipped' }, true)
|
|
114
|
+
|
|
115
|
+
// options object
|
|
116
|
+
await mercure.send('/orders/42', { status: 'shipped' }, { private: true })
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### SSE options (id, type, retry)
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
await mercure.send(
|
|
123
|
+
'/orders/42',
|
|
124
|
+
{ status: 'shipped' },
|
|
125
|
+
{
|
|
126
|
+
id: 'msg-001', // event ID — enables reconnection recovery
|
|
127
|
+
type: 'order.shipped', // event type
|
|
128
|
+
retry: 5000, // client reconnect delay in ms
|
|
129
|
+
private: true,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Generate a subscriber token
|
|
135
|
+
|
|
136
|
+
Use this to create JWT tokens for your frontend clients so they can subscribe to topics, including private ones.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// typed shorthand
|
|
140
|
+
const token = await mercure.generateSubscribeToken(['/orders/42'])
|
|
141
|
+
|
|
142
|
+
// or low-level
|
|
143
|
+
const token = await mercure.generate({ subscribe: ['/orders/42'] })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Pass the token to your frontend:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const url = new URL('http://localhost:3000/.well-known/mercure')
|
|
150
|
+
url.searchParams.append('topic', '/orders/42')
|
|
151
|
+
|
|
152
|
+
const eventSource = new EventSource(url.toString(), {
|
|
153
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Health check
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const isReachable = await mercure.ping() // true | false
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Testing
|
|
166
|
+
|
|
167
|
+
`FakeMercure` lets you test your application code without a real Mercure Hub.
|
|
168
|
+
|
|
169
|
+
**Swap the container binding in your test setup:**
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { FakeMercure } from '@mdsiha369/adonis-mercure'
|
|
173
|
+
|
|
174
|
+
// before your test
|
|
175
|
+
app.container.swap('mercure', () => new FakeMercure())
|
|
176
|
+
|
|
177
|
+
// after your test
|
|
178
|
+
app.container.restore('mercure')
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Assert on what was sent:**
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const fake = (await app.container.make('mercure')) as FakeMercure
|
|
185
|
+
|
|
186
|
+
// assert a topic received a message
|
|
187
|
+
fake.assertSent('/orders/42')
|
|
188
|
+
|
|
189
|
+
// assert a topic received a specific payload
|
|
190
|
+
fake.assertSent('/orders/42', { status: 'shipped' })
|
|
191
|
+
|
|
192
|
+
// assert a topic was never sent to
|
|
193
|
+
fake.assertNotSent('/admin/secret')
|
|
194
|
+
|
|
195
|
+
// assert nothing was sent at all
|
|
196
|
+
fake.assertNothingSent()
|
|
197
|
+
|
|
198
|
+
// inspect all recorded messages
|
|
199
|
+
const messages = fake.getSent()
|
|
200
|
+
|
|
201
|
+
// reset between tests
|
|
202
|
+
fake.clear()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## API Reference
|
|
208
|
+
|
|
209
|
+
### `send(topics, data?, options?)`
|
|
210
|
+
|
|
211
|
+
Publishes an update to the Mercure Hub.
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Default | Description |
|
|
214
|
+
| --------- | ------------------------- | ------- | ------------------------------------------------------ |
|
|
215
|
+
| `topics` | `string \| string[]` | — | Topic(s) to publish to |
|
|
216
|
+
| `data` | `Record<string, unknown>` | `{}` | Payload — serialized as JSON |
|
|
217
|
+
| `options` | `boolean \| SendOptions` | `false` | `true` for private (legacy), or a `SendOptions` object |
|
|
218
|
+
|
|
219
|
+
**`SendOptions`**
|
|
220
|
+
|
|
221
|
+
| Property | Type | Description |
|
|
222
|
+
| --------- | --------- | ---------------------------------------------- |
|
|
223
|
+
| `private` | `boolean` | Restrict delivery to authenticated subscribers |
|
|
224
|
+
| `id` | `string` | SSE event ID (enables reconnection recovery) |
|
|
225
|
+
| `type` | `string` | SSE event type |
|
|
226
|
+
| `retry` | `number` | Client reconnect delay in milliseconds |
|
|
227
|
+
|
|
228
|
+
Throws `MercurePublishError` if the hub returns a non-2xx response.
|
|
229
|
+
Throws `MercureTimeoutError` if the hub does not respond within the configured timeout.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `generateSubscribeToken(topics)`
|
|
234
|
+
|
|
235
|
+
Generates a JWT with `{ subscribe: topics }` for a frontend client.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const token = await mercure.generateSubscribeToken(['/chat/1', '/notifications/me'])
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### `generate(payload)`
|
|
244
|
+
|
|
245
|
+
Low-level JWT generation. Wraps `payload` under the `mercure` claim.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const token = await mercure.generate({ subscribe: ['/chat/1'], publish: ['/chat/1'] })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### `ping()`
|
|
254
|
+
|
|
255
|
+
Returns `true` if the hub is reachable, `false` on network error or timeout.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Roadmap
|
|
260
|
+
|
|
261
|
+
Features planned for upcoming releases:
|
|
262
|
+
|
|
263
|
+
- **`generatePublishToken(topics)`** — typed shorthand for publisher JWT tokens (symmetric to `generateSubscribeToken`)
|
|
264
|
+
- **`sendBatch(messages[])`** — publish multiple updates in a single call
|
|
265
|
+
- **Retry with backoff** — automatic retry on transient hub errors with configurable strategy
|
|
266
|
+
- **Typed event classes** — define reusable event objects (`new OrderShippedEvent(id)`) instead of inline payloads
|
|
267
|
+
|
|
268
|
+
Have a use case or suggestion? [Open an issue](https://github.com/mdsiha/adonis-mercure/issues).
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT — [Michael DAŞ](https://github.com/mdsiha)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Configure hook
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| The configure hook is called when someone runs "node ace configure <package>"
|
|
7
|
+
| command. You are free to perform any operations inside this function to
|
|
8
|
+
| configure the package.
|
|
9
|
+
|
|
|
10
|
+
| To make things easier, you have access to the underlying "ConfigureCommand"
|
|
11
|
+
| instance and you can use codemods to modify the source files.
|
|
12
|
+
|
|
|
13
|
+
*/
|
|
14
|
+
import { stubsRoot } from './stubs/main.js';
|
|
15
|
+
export async function configure(_command) {
|
|
16
|
+
const codemods = await _command.createCodemods();
|
|
17
|
+
await codemods.makeUsingStub(stubsRoot, 'config/mercure.stub', {});
|
|
18
|
+
await codemods.defineEnvVariables({
|
|
19
|
+
MERCURE_ENDPOINT: 'http://localhost:3000/.well-known/mercure',
|
|
20
|
+
MERCURE_ADMIN_JWT: '',
|
|
21
|
+
MERCURE_JWT_SECRET: '',
|
|
22
|
+
});
|
|
23
|
+
await codemods.defineEnvValidations({
|
|
24
|
+
variables: {
|
|
25
|
+
MERCURE_ENDPOINT: `Env.schema.string({ format: 'url' })`,
|
|
26
|
+
MERCURE_ADMIN_JWT: `Env.schema.string()`,
|
|
27
|
+
MERCURE_JWT_SECRET: `Env.schema.string()`,
|
|
28
|
+
},
|
|
29
|
+
leadingComment: 'Mercure configuration',
|
|
30
|
+
});
|
|
31
|
+
await codemods.updateRcFile((rcFile) => {
|
|
32
|
+
rcFile.addProvider('@mdsiha369/adonis-mercure/mercure_provider');
|
|
33
|
+
});
|
|
34
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { configure } from './configure.js';
|
|
2
|
+
export { Mercure } from './src/mercure.js';
|
|
3
|
+
export { FakeMercure } from './src/fake_mercure.js';
|
|
4
|
+
export { defineConfig } from './src/define_config.js';
|
|
5
|
+
export { stubsRoot } from './stubs/main.js';
|
|
6
|
+
export { MercureError } from './src/errors/mercure_error.js';
|
|
7
|
+
export { MercurePublishError } from './src/errors/publish_error.js';
|
|
8
|
+
export { MercureConfigError } from './src/errors/config_error.js';
|
|
9
|
+
export { MercureTimeoutError } from './src/errors/timeout_error.js';
|
package/build/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Package entrypoint
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
*/
|
|
6
|
+
export { configure } from './configure.js';
|
|
7
|
+
export { Mercure } from './src/mercure.js';
|
|
8
|
+
export { FakeMercure } from './src/fake_mercure.js';
|
|
9
|
+
export { defineConfig } from './src/define_config.js';
|
|
10
|
+
export { stubsRoot } from './stubs/main.js';
|
|
11
|
+
export { MercureError } from './src/errors/mercure_error.js';
|
|
12
|
+
export { MercurePublishError } from './src/errors/publish_error.js';
|
|
13
|
+
export { MercureConfigError } from './src/errors/config_error.js';
|
|
14
|
+
export { MercureTimeoutError } from './src/errors/timeout_error.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import '../src/types/extended.js';
|
|
2
|
+
import { Mercure } from '../src/mercure.js';
|
|
3
|
+
import { MercureConfigError } from '../src/errors/config_error.js';
|
|
4
|
+
export default class MercureProvider {
|
|
5
|
+
app;
|
|
6
|
+
constructor(app) {
|
|
7
|
+
this.app = app;
|
|
8
|
+
}
|
|
9
|
+
async boot() {
|
|
10
|
+
this.app.container.singleton('mercure', async () => {
|
|
11
|
+
const config = this.app.config.get('mercure');
|
|
12
|
+
if (!config?.endpoint || !config?.adminToken || !config?.jwt?.secret) {
|
|
13
|
+
throw new MercureConfigError('Missing or incomplete Mercure config. Run "node ace configure @mdsiha369/adonis-mercure"');
|
|
14
|
+
}
|
|
15
|
+
return new Mercure(config);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MercureConfigError } from './errors/config_error.js';
|
|
2
|
+
const ALLOWED_ALGORITHMS = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'];
|
|
3
|
+
export function defineConfig(config) {
|
|
4
|
+
if (!ALLOWED_ALGORITHMS.includes(config.jwt.alg)) {
|
|
5
|
+
throw new MercureConfigError(`Algorithm "${config.jwt.alg}" is not allowed. Use one of: ${ALLOWED_ALGORITHMS.join(', ')}`);
|
|
6
|
+
}
|
|
7
|
+
if (config.jwt.secret.length < 32) {
|
|
8
|
+
throw new MercureConfigError('jwt.secret must be at least 32 characters long');
|
|
9
|
+
}
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MercureError } from './mercure_error.js';
|
|
2
|
+
export class MercurePublishError extends MercureError {
|
|
3
|
+
statusCode;
|
|
4
|
+
responseBody;
|
|
5
|
+
constructor(statusCode, responseBody) {
|
|
6
|
+
super(`Mercure hub returned ${statusCode}`);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.name = 'MercurePublishError';
|
|
9
|
+
this.responseBody = responseBody.slice(0, 200);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MercureError } from './mercure_error.js';
|
|
2
|
+
export class MercureTimeoutError extends MercureError {
|
|
3
|
+
timeoutMs;
|
|
4
|
+
constructor(timeoutMs) {
|
|
5
|
+
super(`Request to Mercure hub timed out after ${timeoutMs}ms`);
|
|
6
|
+
this.timeoutMs = timeoutMs;
|
|
7
|
+
this.name = 'MercureTimeoutError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MercureContract, SendOptions } from './types/main.js';
|
|
2
|
+
export interface SentMessage {
|
|
3
|
+
topics: string[];
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
options: SendOptions;
|
|
6
|
+
}
|
|
7
|
+
export declare class FakeMercure implements MercureContract {
|
|
8
|
+
#private;
|
|
9
|
+
generate(payload: Record<string, unknown>): Promise<string>;
|
|
10
|
+
generateSubscribeToken(topics: string[]): Promise<string>;
|
|
11
|
+
send(topics: string | string[], data?: Record<string, unknown>, isPrivateOrOptions?: boolean | SendOptions): Promise<Response>;
|
|
12
|
+
ping(): Promise<boolean>;
|
|
13
|
+
assertSent(topic: string, data?: Record<string, unknown>): void;
|
|
14
|
+
assertNotSent(topic: string): void;
|
|
15
|
+
assertNothingSent(): void;
|
|
16
|
+
getSent(): SentMessage[];
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class FakeMercure {
|
|
2
|
+
#sent = [];
|
|
3
|
+
#tokens = [];
|
|
4
|
+
async generate(payload) {
|
|
5
|
+
this.#tokens.push(payload);
|
|
6
|
+
return 'fake-token';
|
|
7
|
+
}
|
|
8
|
+
async generateSubscribeToken(topics) {
|
|
9
|
+
return this.generate({ subscribe: topics });
|
|
10
|
+
}
|
|
11
|
+
async send(topics, data = {}, isPrivateOrOptions = false) {
|
|
12
|
+
const topicList = Array.isArray(topics) ? topics : [topics];
|
|
13
|
+
const options = typeof isPrivateOrOptions === 'boolean' ? { private: isPrivateOrOptions } : isPrivateOrOptions;
|
|
14
|
+
this.#sent.push({ topics: topicList, data, options });
|
|
15
|
+
return new Response('1', { status: 200 });
|
|
16
|
+
}
|
|
17
|
+
async ping() {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
assertSent(topic, data) {
|
|
21
|
+
const match = this.#sent.find((msg) => {
|
|
22
|
+
if (!msg.topics.includes(topic))
|
|
23
|
+
return false;
|
|
24
|
+
if (!data)
|
|
25
|
+
return true;
|
|
26
|
+
return JSON.stringify(msg.data) === JSON.stringify(data);
|
|
27
|
+
});
|
|
28
|
+
if (!match) {
|
|
29
|
+
const found = this.#sent.map((m) => m.topics.join(', ')).join('; ') || 'nothing';
|
|
30
|
+
throw new Error(`Expected a message on topic "${topic}" but found: [${found}]`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
assertNotSent(topic) {
|
|
34
|
+
const match = this.#sent.find((msg) => msg.topics.includes(topic));
|
|
35
|
+
if (match) {
|
|
36
|
+
throw new Error(`Expected no message on topic "${topic}" but one was found`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
assertNothingSent() {
|
|
40
|
+
if (this.#sent.length > 0) {
|
|
41
|
+
const topics = this.#sent.map((m) => m.topics.join(', ')).join('; ');
|
|
42
|
+
throw new Error(`Expected nothing to be sent but found messages on: [${topics}]`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
getSent() {
|
|
46
|
+
return [...this.#sent];
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
this.#sent = [];
|
|
50
|
+
this.#tokens = [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MercureConfig, MercureContract, SendOptions } from './types/main.js';
|
|
2
|
+
export declare class Mercure implements MercureContract {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(config: MercureConfig);
|
|
5
|
+
generate(payload: Record<string, unknown>): Promise<string>;
|
|
6
|
+
generateSubscribeToken(topics: string[]): Promise<string>;
|
|
7
|
+
send(topics: string | string[], data?: Record<string, unknown>, isPrivateOrOptions?: boolean | SendOptions): Promise<Response>;
|
|
8
|
+
ping(): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TokenGenerator } from './token_generator.js';
|
|
2
|
+
import { Publisher } from './publisher.js';
|
|
3
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
4
|
+
export class Mercure {
|
|
5
|
+
#config;
|
|
6
|
+
#tokenGenerator;
|
|
7
|
+
#publisher;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.#config = config;
|
|
10
|
+
this.#tokenGenerator = new TokenGenerator(config);
|
|
11
|
+
this.#publisher = new Publisher(config);
|
|
12
|
+
}
|
|
13
|
+
generate(payload) {
|
|
14
|
+
return this.#tokenGenerator.generate(payload);
|
|
15
|
+
}
|
|
16
|
+
generateSubscribeToken(topics) {
|
|
17
|
+
return this.#tokenGenerator.generateSubscribeToken(topics);
|
|
18
|
+
}
|
|
19
|
+
send(topics, data = {}, isPrivateOrOptions = false) {
|
|
20
|
+
const options = typeof isPrivateOrOptions === 'boolean' ? { private: isPrivateOrOptions } : isPrivateOrOptions;
|
|
21
|
+
return this.#publisher.publish(topics, data, options);
|
|
22
|
+
}
|
|
23
|
+
async ping() {
|
|
24
|
+
const timeout = this.#config.http?.timeout ?? DEFAULT_TIMEOUT;
|
|
25
|
+
const controller = timeout > 0 ? new AbortController() : null;
|
|
26
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeout) : null;
|
|
27
|
+
try {
|
|
28
|
+
await fetch(this.#config.endpoint, {
|
|
29
|
+
method: 'HEAD',
|
|
30
|
+
signal: controller?.signal,
|
|
31
|
+
});
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (timer)
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|