Skip to content

Commit

Permalink
feat(sse): add @tsed/sse package
Browse files Browse the repository at this point in the history
  • Loading branch information
Romakita committed Aug 1, 2024
1 parent 3096b89 commit 3a2a1ad
Show file tree
Hide file tree
Showing 26 changed files with 1,324 additions and 64 deletions.
6 changes: 6 additions & 0 deletions docs/.vuepress/config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ module.exports = ({title, description, base = "", url, apiRedirectUrl = "", them
text: "Stripe",
link: `${base}/tutorials/stripe.html`
},
{
text: "Server-sent events",
link: `${base}/tutorials/server-sent events.html`
},
{
text: "Agenda",
link: `${base}/tutorials/agenda.html`
Expand Down Expand Up @@ -441,6 +445,7 @@ module.exports = ({title, description, base = "", url, apiRedirectUrl = "", them
{title: "Stripe", path: base + "/tutorials/stripe"},
{title: "Agenda", path: base + "/tutorials/agenda"},
{title: "Terminus", path: base + "/tutorials/terminus"},
{title: "Server-sent events", path: base + "/tutorials/server-sent-events"},
{title: "Serverless", path: base + "/tutorials/serverless"},
{title: "IORedis", path: base + "/tutorials/ioredis"},
{title: "Objection.js", path: base + "/tutorials/objection"},
Expand Down Expand Up @@ -490,6 +495,7 @@ module.exports = ({title, description, base = "", url, apiRedirectUrl = "", them
base + "/tutorials/agenda",
base + "/tutorials/terminus",
base + "/tutorials/serverless",
base + "/tutorials/server-sent-events",
base + "/tutorials/ioredis",
base + "/tutorials/vike",
base + "/tutorials/jest",
Expand Down
36 changes: 23 additions & 13 deletions docs/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,29 @@ Here are the plugins provided by Ts.ED and the compatibility with the different

<div class="table-features">

| Features | Express.js | Koa.js | [Serverless λ](/docs/platform-serverless.md) | [CLI](/docs/command.md) |
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ |
| [Passport.js](/tutorials/passport.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Prisma](/tutorials/prisma.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [TypeORM](/tutorials/typeorm.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Mongoose](/tutorials/mongoose.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [IORedis](/tutorials/ioredis.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Objection.js](/tutorials/objection.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [GraphQL](/tutorials/graphql.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Socket.io](/tutorials/socket-io.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Swagger](/tutorials/swagger.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [OIDC](/tutorials/oidc.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Stripe](/tutorials/stripe.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <center>?</center> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| Features | Express.js | Koa.js | [Serverless λ](/docs/platform-serverless.md) | [CLI](/docs/command.md) |
| ------------------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ |
| [Agenda](/tutorials/agenda.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Bulk MQ](/tutorials/bulkmq.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Event emitter](https://www.npmjs.com/package/@tsed/event-emitter) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [IORedis](/tutorials/ioredis.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Formio](https://www.npmjs.com/package/@tsed/formio) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <center>?</center> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [GraphQL](/tutorials/graphql.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Mikro ORM](/tutorials/mikroorm.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Mongoose](/tutorials/mongoose.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Objection.js](/tutorials/objection.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [OIDC](/tutorials/oidc.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Pulse](/tutorials/pulse.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Passport.js](/tutorials/passport.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Prisma](/tutorials/prisma.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Socket.io](/tutorials/socket-io.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Swagger](/tutorials/swagger.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Stripe](/tutorials/stripe.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Server-sent events](/tutorials/server-sent-events.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Temporal](/tutorials/temporal.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [Terminus](/tutorials/terminus.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |
| [TypeORM](/tutorials/typeorm.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> |
| [Vike](/tutorials/vike.html) | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/valid.svg" width="15" alt="yes"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> | <img src="../assets/invalid.svg" width="15" alt="no"/> |

</div>

Expand Down
171 changes: 171 additions & 0 deletions docs/tutorials/server-sent-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
meta:
- name: description
content: Guide to implement Server-sent events with Ts.ED.
- name: keywords
content: ts.ed express typescript sse server-sent events node.js javascript decorators
---

# Server-sent events

Server-sent events let you push data to the client. It's a simple way to send data from the server to the client. The data is sent as a stream of messages, with an optional event name and id. It's a simple way to send data from the server to the client.

## Installation

Before using the Server-sent events, we need to install the `@tsed/sse` module.

<Tabs class="-code">
<Tab label="npm">

```bash
npm install --save @tsed/sse
```

</Tab>

<Tab label="yarn">

```bash
yarn add --save @tsed/sse
```

</Tab>
<Tab label="pnpm">

```bash
pnpm add --save @tsed/sse
```

</Tab>
<Tab label="bun">

```bash
bun add --save @tsed/sse
```

</Tab>
</Tabs>

Then add the following configuration in your Server:

```typescript
import {Configuration} from "@tsed/common";
import "@tsed/sse"; // import sse Ts.ED module

@Configuration({
acceptMimes: ["application/json", "text/event-stream"]
})
export class Server {}
```

::: warning
There is a known issue with the `compression` middleware. The
`compression` middleware should be disabled to work correctly with Server-sent events.
:::

## Features

- Support decorator usage to enable event-stream on an endpoint,
- Support Node.js stream like `EventEmmiter` to emit events from your controller to your consumer,
- Support `Observable` from `rxjs` to emit events from your controller to your consumer.
- Support `@tsed/json-mapper` to serialize your model before sending it to the client.
- Gives an API compatible with Express.js and Koa.js.

## Enable event-stream

To enable the event-stream on an endpoint, you need to use the `@EventStream()` decorator on a method of a controller.

```typescript
import {Controller} from "@tsed/di";
import {Get} from "@tsed/schema";
import {EventStream, EventStreamCtx} from "@tsed/sse";

@Controller("/sse")
export class MyCtrl {
@Get("/events")
@EventStream()
events(@EventStreamCtx() eventStream: EventStreamCtx) {
let intervalId: ReturnType<typeof setInterval>;

eventStream.on("close", () => {
clearInterval(intervalId);
});

eventStream.on("end", () => {
clearInterval(intervalId);
});

intervalId = setInterval(() => {
// Ts.ED support Model serialization using json-mapper here
eventStream.emit("event", new Date());
}, 1000);
}
}
```

### Stream events

You can use Node.js stream like `EventEmmiter` to emit events from your controller to your consumer:

```ts
import {EventStream} from "@tsed/sse";
import {Controller} from "@tsed/di";
import {Get} from "@tsed/schema";

@Controller("/sse")
export class MyCtrl {
private eventEmitter = new EventEmitter();

$onInit() {
setInterval(() => {
this.eventEmitter.emit("message", new Date());
}, 1000);
}

@Get("/events")
@EventStream()
events() {
return this.eventEmitter;
}
}
```

### Observable

You can also use `Observable` from `rxjs` to emit events from your controller to your consumer:

```ts
import {Controller} from "@tsed/di";
import {Get} from "@tsed/schema";
import {EventStream} from "@tsed/sse";
import {Observable} from "rxjs";

@Controller("/sse")
export class MyCtrl {
@Get("/events")
@EventStream()
events() {
const observable = new Observable((observer) => {
setInterval(() => {
observer.next(new Date());
}, 1000);
});

return observable;
}
}
```

## Author

<GithubContributors :users="['Romakita']"/>

## Maintainers <Badge text="Help wanted" />

<GithubContributors :users="['Romakita']"/>

<div class="flex items-center justify-center p-5">
<Button href="/contributing.html" class="rounded-medium">
Become maintainer
</Button>
</div>
2 changes: 1 addition & 1 deletion docs/tutorials/serverless.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
meta:
- name: description
content: Guide to deploy your Ts.ED application on Serveless.
content: Guide to deploy your Ts.ED application on Serverless.
- name: keywords
content: ts.ed express typescript aws node.js javascript decorators
projects:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"test:graphql": "lerna run test --scope '@tsed/{apollo,typegraphql}' --stream",
"test:security": "lerna run test --scope '@tsed/{jwks,oidc-provider,passport,oidc-provider-plugin-wildcard-redirect-uri}' --stream",
"test:specs": "lerna run test --scope '@tsed/{ajv,exceptions,json-mapper,schema,swagger}' --stream --concurrency 2",
"test:third-parties": "lerna run test --scope '@tsed/{agenda,bullmq,async-hook-context,components-scan,event-emitter,seq,socketio,stripe,temporal,terminus,vite-ssr-plugin}' --stream --concurrency 4",
"test:third-parties": "lerna run test --scope '@tsed/{agenda,bullmq,components-scan,event-emitter,formio,pulse,sse,socketio,stripe,temporal,terminus,vike,vite-ssr-plugin}' --stream --concurrency 4",
"test:formio": "lerna run test --scope '@tsed/{schema-formio,formio}' --stream",
"coverage": "merge-istanbul --out coverage/coverage-final.json \"**/packages/**/coverage/coverage-final.json\" && nyc report --reporter text --reporter html --reporter lcov -t coverage --report-dir coverage",
"barrels": "lerna run barrels",
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/common/.barrelsby.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"directory": ["./src"],
"exclude": ["**/__mock__", "**/__mocks__", "**/*.spec.ts", "**/getConfiguration.ts"],
"exclude": ["**/__mock__", "**/__mocks__", "**/*.spec.ts", "**/getConfiguration.ts", "FakeResponse.ts"],
"delete": true
}
72 changes: 72 additions & 0 deletions packages/platform/common/src/services/FakeResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {EventEmitter} from "node:events";

export class FakeResponse extends EventEmitter {
headers: Record<string, unknown> = {};
locals: Record<string, unknown> = {};
statusCode: number = 200;
data: any;

constructor(opts = {}) {
super();

Object.assign(this, opts);
}

status(code: number) {
this.statusCode = code;
return this;
}

contentType(content: string) {
this.set("content-type", content);
}

contentLength(content: number) {
this.set("content-length", content);
}

redirect(status: number, path: string) {
this.statusCode = status;
this.set("location", path);
}

location(path: string) {
this.set("location", path);
}

get(key: string) {
return this.headers[key.toLowerCase()];
}

getHeaders() {
return this.headers;
}

set(key: string, value: any) {
this.headers[key.toLowerCase()] = value;
return this;
}

setHeader(key: string, value: any) {
this.headers[key.toLowerCase()] = value;
return this;
}

send(data: any) {
this.data = data;
}

json(data: any) {
this.data = data;
}

write(chunk: any) {
this.emit("data", chunk);
}

end(data: any) {
data !== undefined && (this.data = data);

this.emit("end");
}
}
Loading

0 comments on commit 3a2a1ad

Please sign in to comment.