Skip to content

Commit 3b11e32

Browse files
committed
feat(sse): add server-sent events endpoint
1 parent c0a3863 commit 3b11e32

File tree

15 files changed

+199
-26
lines changed

15 files changed

+199
-26
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ The API will start automatically when the Bree constructor is called.
3939

4040
## Options
4141

42-
| Option | Type | Description |
43-
|:------:|:------:|----------------------------------------------------------------------------------------------|
44-
| port | Number | The port the API will listen on. Default: `62893` |
45-
| jwt | Object | Configurations for JWT. Only option is `secret` which will be the secret used to verify JWT. |
42+
| Option | Type | Description |
43+
| :------: | :------: | ---------------------------------------------------------------------------------------------- |
44+
| port | Number | The port the API will listen on. Default: `62893` |
45+
| jwt | Object | Configurations for JWT. Only option is `secret` which will be the secret used to verify JWT. |
46+
| sse | Object | Configurations for SSE. See [koa-sse][] for list of options. |
4647

4748
## API
4849

@@ -62,3 +63,5 @@ Check out the [API Docs](https://documenter.getpostman.com/view/17142435/TzzDLbN
6263
[yarn]: https://yarnpkg.com/
6364

6465
[Bree]: https://jobscheduler.net/#/
66+
67+
[koa-sse]: https://github.com/yklykl530/koa-sse

api.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const ip = require('ip');
1010
const logger = require('./helpers/logger');
1111
const apiConfig = require('./config/api');
1212

13-
const api = new API(apiConfig);
13+
const api = new API(apiConfig());
1414

1515
if (!module.parent) {
1616
const graceful = new Graceful({
@@ -25,7 +25,7 @@ if (!module.parent) {
2525
if (process.send) process.send('ready');
2626
const { port } = api.server.address();
2727
logger.info(
28-
`Lad API server listening on ${port} (LAN: ${ip.address()}:${port})`
28+
`Bree API server listening on ${port} (LAN: ${ip.address()}:${port})`
2929
);
3030
} catch (error) {
3131
logger.error(error);

app/controllers/api/v1/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const config = require('./config');
22
const jobs = require('./jobs');
33
const control = require('./control');
4+
const sse = require('./sse');
45

56
const test = (ctx) => {
67
ctx.body = { breeExists: Boolean(ctx.bree) };
78
};
89

9-
module.exports = { config, test, jobs, control };
10+
module.exports = { config, test, jobs, control, sse };

app/controllers/api/v1/sse.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
async function connect(ctx) {
2+
if (ctx.sse) {
3+
// send bree events over sse
4+
for (const event of ['worker created', 'worker deleted']) {
5+
ctx.bree.on(event, (name) => {
6+
ctx.sse.send({ event, data: { name } });
7+
});
8+
}
9+
10+
ctx.sse.on('close', () => {
11+
ctx.logger.error('SSE closed');
12+
});
13+
}
14+
}
15+
16+
module.exports = { connect };

config/api.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,54 @@
11
const sharedConfig = require('@ladjs/shared-config');
22
const jwt = require('koa-jwt');
3+
const sse = require('koa-sse-stream');
34

45
const logger = require('../helpers/logger');
56
const routes = require('../routes');
67
const config = require('../config');
78

8-
module.exports = {
9-
...sharedConfig('API'),
10-
routes: routes.api,
11-
logger,
12-
hookBeforeRoutes(app) {
13-
app.use(jwt(config.jwt));
14-
}
9+
module.exports = (opts = {}) => {
10+
const sseMiddleware = sse({ ...config.sse, ...opts.sse });
11+
const jwtMiddleware = jwt({
12+
...config.jwt,
13+
...opts.jwt,
14+
getToken(ctx, _) {
15+
// pull token off of url if it is the sse endpoint
16+
if (ctx.url.indexOf('/v1/sse') === 0) {
17+
const splitUrl = ctx.url.split('/');
18+
19+
if (splitUrl.length === 4) {
20+
return splitUrl[3];
21+
}
22+
}
23+
24+
return null;
25+
}
26+
});
27+
28+
return {
29+
...sharedConfig('API'),
30+
port: config.port,
31+
...opts,
32+
routes: routes.api,
33+
logger,
34+
hookBeforeRoutes(app) {
35+
app.use((ctx, next) => {
36+
// return early if jwt is set to false
37+
if (!opts.jwt && typeof opts.jwt === 'boolean') {
38+
return next();
39+
}
40+
41+
return jwtMiddleware(ctx, next);
42+
});
43+
44+
app.use((ctx, next) => {
45+
// only do this on sse route
46+
if (ctx.url.indexOf('/v1/sse') === 0) {
47+
return sseMiddleware(ctx, next);
48+
}
49+
50+
return next();
51+
});
52+
}
53+
};
1554
};

config/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const config = {
3030
secret: env.JWT_SECRET
3131
},
3232

33+
// sse options
34+
sse: {
35+
maxClients: 10_000,
36+
pingInterval: 60_000
37+
},
38+
3339
// store IP address
3440
// <https://github.com/ladjs/store-ip-address>
3541
storeIPAddress: {

index.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const API = require('@ladjs/api');
2-
const jwt = require('koa-jwt');
32

43
const api = require('./api');
54
const apiConfig = require('./config/api');
@@ -10,17 +9,11 @@ function plugin(opts, Bree) {
109
opts = {
1110
port: config.port,
1211
jwt: config.jwt,
12+
sse: config.sse,
1313
...opts
1414
};
1515

16-
const api = new API({
17-
...apiConfig,
18-
port: opts.port,
19-
jwt: opts.jwt,
20-
hookBeforeRoutes(app) {
21-
app.use(jwt(opts.jwt));
22-
}
23-
});
16+
const api = new API(apiConfig(opts));
2417

2518
const oldInit = Bree.prototype.init;
2619

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"humanize-string": "^3.0.0",
3535
"ip": "^1.1.5",
3636
"koa-jwt": "^4.0.1",
37+
"koa-sse-stream": "^0.2.0",
3738
"lodash": "^4.17.20",
3839
"markdown-it": "^13.0.1",
3940
"markdown-it-emoji": "^2.0.0",
@@ -55,6 +56,7 @@
5556
"eslint-formatter-pretty": "^4.0.0",
5657
"eslint-plugin-compat": "^4.0.2",
5758
"eslint-plugin-no-smart-quotes": "^1.1.0",
59+
"eventsource": "^2.0.2",
5860
"fixpack": "^4.0.0",
5961
"get-port": "^5.1.1",
6062
"gulp": "^4.0.2",

routes/api/v1/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ router.post('/run/:jobName', api.v1.control.run);
2929
router.post('/restart', api.v1.control.restart);
3030
router.post('/restart/:jobName', api.v1.control.restart);
3131

32+
router.get('/sse/:token', api.v1.sse.connect);
33+
3234
module.exports = router;

test/api/v1/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ test.before(async (t) => {
1313
test('fails when no creds are presented', async (t) => {
1414
const { api } = t.context;
1515
const res = await api.get('/v1/test');
16+
1617
t.is(res.status, 401);
1718
});
1819

0 commit comments

Comments
 (0)