@tsed/sse 8.0.0 → 8.0.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/package.json +16 -15
- package/src/__snapshots__/sse.integration.spec.ts.snap +171 -0
- package/src/decorators/eventStream.ts +19 -0
- package/src/decorators/eventStreamCtx.ts +9 -0
- package/src/domain/EventStreamContext.ts +133 -0
- package/src/index.ts +8 -0
- package/src/interceptors/EventStreamInterceptor.ts +34 -0
- package/src/middlewares/EventStreamMiddleware.ts +25 -0
- package/src/sse.integration.spec.ts +207 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tsed/sse",
|
|
3
3
|
"description": "Server-sent events package for Ts.ED framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tsed",
|
|
8
8
|
"server-sent events",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"typings": "./lib/types/index.d.ts",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": {
|
|
19
|
+
"@tsed/source": "./src/index.ts",
|
|
19
20
|
"types": "./lib/types/index.d.ts",
|
|
20
21
|
"import": "./lib/esm/index.js",
|
|
21
22
|
"default": "./lib/esm/index.js"
|
|
@@ -33,27 +34,27 @@
|
|
|
33
34
|
"tslib": "2.7.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@tsed/barrels": "8.0.
|
|
37
|
-
"@tsed/core": "8.0.
|
|
38
|
-
"@tsed/di": "8.0.
|
|
39
|
-
"@tsed/json-mapper": "8.0.
|
|
40
|
-
"@tsed/openspec": "8.0.
|
|
41
|
-
"@tsed/platform-http": "8.0.
|
|
42
|
-
"@tsed/platform-middlewares": "8.0.
|
|
43
|
-
"@tsed/schema": "8.0.
|
|
44
|
-
"@tsed/socketio-testing": "8.0.
|
|
45
|
-
"@tsed/typescript": "8.0.
|
|
37
|
+
"@tsed/barrels": "8.0.2",
|
|
38
|
+
"@tsed/core": "8.0.2",
|
|
39
|
+
"@tsed/di": "8.0.2",
|
|
40
|
+
"@tsed/json-mapper": "8.0.2",
|
|
41
|
+
"@tsed/openspec": "8.0.2",
|
|
42
|
+
"@tsed/platform-http": "8.0.2",
|
|
43
|
+
"@tsed/platform-middlewares": "8.0.2",
|
|
44
|
+
"@tsed/schema": "8.0.2",
|
|
45
|
+
"@tsed/socketio-testing": "8.0.2",
|
|
46
|
+
"@tsed/typescript": "8.0.2",
|
|
46
47
|
"eslint": "9.12.0",
|
|
47
48
|
"express": "^4.21.0",
|
|
48
49
|
"typescript": "5.4.5",
|
|
49
50
|
"vitest": "2.1.2"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
|
-
"@tsed/di": "8.0.
|
|
53
|
-
"@tsed/json-mapper": "8.0.
|
|
53
|
+
"@tsed/di": "8.0.2",
|
|
54
|
+
"@tsed/json-mapper": "8.0.2",
|
|
54
55
|
"@tsed/logger": ">=6.7.5",
|
|
55
|
-
"@tsed/platform-middlewares": "8.0.
|
|
56
|
-
"@tsed/schema": "8.0.
|
|
56
|
+
"@tsed/platform-middlewares": "8.0.2",
|
|
57
|
+
"@tsed/schema": "8.0.2"
|
|
57
58
|
},
|
|
58
59
|
"peerDependenciesMeta": {
|
|
59
60
|
"@tsed/di": {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SSE integration > should generate the swagger spec 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"components": {
|
|
6
|
+
"schemas": {
|
|
7
|
+
"Model": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"type": "object",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"paths": {
|
|
18
|
+
"/sse/scenario1": {
|
|
19
|
+
"get": {
|
|
20
|
+
"operationId": "myCtrlScenario1",
|
|
21
|
+
"parameters": [],
|
|
22
|
+
"responses": {
|
|
23
|
+
"200": {
|
|
24
|
+
"content": {
|
|
25
|
+
"*/*": {
|
|
26
|
+
"schema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"headers": {
|
|
32
|
+
"Content-Type": {
|
|
33
|
+
"example": "text/event-stream",
|
|
34
|
+
"schema": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"tags": [
|
|
42
|
+
"MyCtrl",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
"/sse/scenario2": {
|
|
47
|
+
"get": {
|
|
48
|
+
"operationId": "myCtrlScenario2",
|
|
49
|
+
"parameters": [],
|
|
50
|
+
"responses": {
|
|
51
|
+
"200": {
|
|
52
|
+
"content": {
|
|
53
|
+
"*/*": {
|
|
54
|
+
"schema": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"headers": {
|
|
60
|
+
"Content-Type": {
|
|
61
|
+
"example": "text/event-stream",
|
|
62
|
+
"schema": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"tags": [
|
|
70
|
+
"MyCtrl",
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"/sse/scenario3": {
|
|
75
|
+
"get": {
|
|
76
|
+
"operationId": "myCtrlScenario3",
|
|
77
|
+
"parameters": [],
|
|
78
|
+
"responses": {
|
|
79
|
+
"200": {
|
|
80
|
+
"content": {
|
|
81
|
+
"*/*": {
|
|
82
|
+
"schema": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"headers": {
|
|
88
|
+
"Content-Type": {
|
|
89
|
+
"example": "text/event-stream",
|
|
90
|
+
"schema": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
"tags": [
|
|
98
|
+
"MyCtrl",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"/sse/scenario4": {
|
|
103
|
+
"get": {
|
|
104
|
+
"operationId": "myCtrlScenario4",
|
|
105
|
+
"parameters": [],
|
|
106
|
+
"responses": {
|
|
107
|
+
"200": {
|
|
108
|
+
"content": {
|
|
109
|
+
"*/*": {
|
|
110
|
+
"schema": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"headers": {
|
|
116
|
+
"Content-Type": {
|
|
117
|
+
"example": "text/event-stream",
|
|
118
|
+
"schema": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"tags": [
|
|
126
|
+
"MyCtrl",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"/sse/scenario5": {
|
|
131
|
+
"get": {
|
|
132
|
+
"operationId": "myCtrlScenario5",
|
|
133
|
+
"parameters": [],
|
|
134
|
+
"responses": {
|
|
135
|
+
"200": {
|
|
136
|
+
"content": {
|
|
137
|
+
"*/*": {
|
|
138
|
+
"schema": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
"application/json": {
|
|
143
|
+
"schema": {
|
|
144
|
+
"$ref": "#/components/schemas/Model",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"description": "Success",
|
|
149
|
+
"headers": {
|
|
150
|
+
"Content-Type": {
|
|
151
|
+
"example": "text/event-stream",
|
|
152
|
+
"schema": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
"tags": [
|
|
160
|
+
"MyCtrl",
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
"tags": [
|
|
166
|
+
{
|
|
167
|
+
"name": "MyCtrl",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {StoreSet, useDecorators} from "@tsed/core";
|
|
2
|
+
import {Use} from "@tsed/platform-middlewares";
|
|
3
|
+
import {AcceptMime, Header} from "@tsed/schema";
|
|
4
|
+
|
|
5
|
+
import {EventStreamMiddleware} from "../middlewares/EventStreamMiddleware.js";
|
|
6
|
+
|
|
7
|
+
export interface EventStreamOpts {
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
event?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EventStream(opts: EventStreamOpts = {}) {
|
|
13
|
+
return useDecorators(
|
|
14
|
+
StoreSet(EventStreamMiddleware, opts),
|
|
15
|
+
Use(EventStreamMiddleware),
|
|
16
|
+
AcceptMime("text/event-stream"),
|
|
17
|
+
Header("Content-Type", "text/event-stream")
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {isBoolean, isDate, isNumber, isString} from "@tsed/core";
|
|
2
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
3
|
+
import {PlatformResponseFilter} from "@tsed/platform-response-filter";
|
|
4
|
+
import type {EventEmitter} from "events";
|
|
5
|
+
import type {Observable} from "rxjs";
|
|
6
|
+
|
|
7
|
+
import type {EventStreamOpts} from "../decorators/eventStream.js";
|
|
8
|
+
|
|
9
|
+
export type EventStreamContextOptions = {
|
|
10
|
+
$ctx: PlatformContext;
|
|
11
|
+
} & EventStreamOpts;
|
|
12
|
+
|
|
13
|
+
export class EventStreamContext {
|
|
14
|
+
private $ctx: PlatformContext;
|
|
15
|
+
private opts: EventStreamOpts;
|
|
16
|
+
private responseFilter: PlatformResponseFilter;
|
|
17
|
+
#isDone = false;
|
|
18
|
+
|
|
19
|
+
constructor({$ctx, ...opts}: EventStreamContextOptions) {
|
|
20
|
+
this.$ctx = $ctx;
|
|
21
|
+
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
this.responseFilter = this.$ctx.injector.get<PlatformResponseFilter>(PlatformResponseFilter)!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
writeHead(headers: Record<string, string> = {}) {
|
|
27
|
+
console.log("this.$ctx", this.$ctx);
|
|
28
|
+
this.on("end", () => {
|
|
29
|
+
this.$ctx.logger.info({
|
|
30
|
+
event: "SSE_ENDED"
|
|
31
|
+
});
|
|
32
|
+
this.$ctx.getRes().end();
|
|
33
|
+
this.#isDone = true;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.on("close", () => {
|
|
37
|
+
this.$ctx.logger.info({
|
|
38
|
+
event: "SSE_CLOSED_BY_CLIENT"
|
|
39
|
+
});
|
|
40
|
+
this.$ctx.getRes().end();
|
|
41
|
+
this.#isDone = true;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.$ctx.getRes().writeHead(200, {
|
|
45
|
+
...this.opts.headers,
|
|
46
|
+
...headers,
|
|
47
|
+
"Content-Type": "text/event-stream",
|
|
48
|
+
"Cache-Control": "no-cache",
|
|
49
|
+
Connection: "keep-alive"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
on(event: string, listener: (...args: any[]) => void): this {
|
|
54
|
+
this.$ctx.getRes().on(event, listener);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
subscribe(observable: Observable<unknown>) {
|
|
59
|
+
observable.subscribe((data) => {
|
|
60
|
+
this.emit(this.opts.event, data);
|
|
61
|
+
});
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stream(stream: NodeJS.ReadableStream | EventEmitter) {
|
|
66
|
+
const listener = (data: unknown) => {
|
|
67
|
+
if (this.isDone()) {
|
|
68
|
+
stream.off("data", listener);
|
|
69
|
+
} else {
|
|
70
|
+
this.emit(this.opts.event, data);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
stream.on("data", listener);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
emit(event: string | undefined, data: unknown) {
|
|
78
|
+
if (this.isDone()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const {$ctx} = this;
|
|
82
|
+
if (data === undefined) {
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Buffer.isBuffer(data)) {
|
|
87
|
+
data = data.toString("utf8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isBoolean(data) || isNumber(data) || isString(data) || isDate(data) || data === null) {
|
|
91
|
+
this.write({
|
|
92
|
+
event: event || "",
|
|
93
|
+
data: JSON.stringify(data)
|
|
94
|
+
});
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.responseFilter
|
|
99
|
+
.serialize(data, $ctx as any)
|
|
100
|
+
.then((data: unknown) => {
|
|
101
|
+
return this.responseFilter.transform(data, $ctx as any);
|
|
102
|
+
})
|
|
103
|
+
.then((data: unknown) => {
|
|
104
|
+
data = JSON.stringify(data);
|
|
105
|
+
|
|
106
|
+
this.write({
|
|
107
|
+
event: event || "",
|
|
108
|
+
data: data as string
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
write(obj: Record<string, string>) {
|
|
116
|
+
const values = Object.entries(obj)
|
|
117
|
+
.map(([key, value]) => {
|
|
118
|
+
return `${key}: ${value}`;
|
|
119
|
+
})
|
|
120
|
+
.join("\n");
|
|
121
|
+
|
|
122
|
+
this.$ctx.getRes().write(`${values}\n\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
isDone() {
|
|
126
|
+
return this.#isDone;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
end() {
|
|
130
|
+
this.#isDone = true;
|
|
131
|
+
this.$ctx.getRes().end();
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./decorators/eventStream.js";
|
|
5
|
+
export * from "./decorators/eventStreamCtx.js";
|
|
6
|
+
export * from "./domain/EventStreamContext.js";
|
|
7
|
+
export * from "./interceptors/EventStreamInterceptor.js";
|
|
8
|
+
export * from "./middlewares/EventStreamMiddleware.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {isObservable, isStream} from "@tsed/core";
|
|
2
|
+
import type {InterceptorContext, InterceptorMethods} from "@tsed/di";
|
|
3
|
+
import {Injectable, InjectContext} from "@tsed/di";
|
|
4
|
+
import type {PlatformContext} from "@tsed/platform-http";
|
|
5
|
+
import type {Observable} from "rxjs";
|
|
6
|
+
|
|
7
|
+
import {EventStreamContext} from "../domain/EventStreamContext.js";
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class EventStreamInterceptor implements InterceptorMethods {
|
|
11
|
+
@InjectContext()
|
|
12
|
+
protected $ctx: PlatformContext;
|
|
13
|
+
|
|
14
|
+
async intercept(context: InterceptorContext): Promise<unknown> {
|
|
15
|
+
const result = await context.next();
|
|
16
|
+
const eventStream = this.$ctx.get<EventStreamContext>("EventStreamContext");
|
|
17
|
+
|
|
18
|
+
if (eventStream) {
|
|
19
|
+
if (isObservable(result)) {
|
|
20
|
+
eventStream.subscribe(result as Observable<unknown>);
|
|
21
|
+
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isStream(result)) {
|
|
26
|
+
eventStream.stream(result as NodeJS.ReadableStream);
|
|
27
|
+
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
import {Middleware} from "@tsed/platform-middlewares";
|
|
3
|
+
import {Context} from "@tsed/platform-params";
|
|
4
|
+
|
|
5
|
+
import {EventStreamContext} from "../domain/EventStreamContext.js";
|
|
6
|
+
|
|
7
|
+
@Middleware()
|
|
8
|
+
export class EventStreamMiddleware {
|
|
9
|
+
use(@Context() $ctx: PlatformContext) {
|
|
10
|
+
const opts = $ctx.endpoint.store.get(EventStreamMiddleware, {});
|
|
11
|
+
|
|
12
|
+
// Enable stream logs
|
|
13
|
+
$ctx.logger.flush();
|
|
14
|
+
$ctx.logger.maxStackSize = 0;
|
|
15
|
+
|
|
16
|
+
const eventStream = new EventStreamContext({
|
|
17
|
+
$ctx,
|
|
18
|
+
...opts
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
$ctx.set("EventStreamContext", eventStream);
|
|
22
|
+
|
|
23
|
+
eventStream.writeHead();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {Controller} from "@tsed/di";
|
|
2
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
3
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
4
|
+
import {Context} from "@tsed/platform-params";
|
|
5
|
+
import {Get, getSpec, JsonEntityStore, Name, Returns} from "@tsed/schema";
|
|
6
|
+
|
|
7
|
+
import {EventStream} from "./decorators/eventStream.js";
|
|
8
|
+
import {EventStreamCtx} from "./decorators/eventStreamCtx.js";
|
|
9
|
+
import {EventStreamContext} from "./domain/EventStreamContext.js";
|
|
10
|
+
|
|
11
|
+
function make(eventStream: EventStreamCtx, cb: () => any) {
|
|
12
|
+
let intervalId: ReturnType<typeof setInterval>;
|
|
13
|
+
|
|
14
|
+
eventStream.on("close", () => {
|
|
15
|
+
clearInterval(intervalId);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
eventStream.on("end", () => {
|
|
19
|
+
clearInterval(intervalId);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
intervalId = setInterval(() => {
|
|
23
|
+
// Ts.ED support Model serialization using json-mapper here
|
|
24
|
+
eventStream.emit("event", cb());
|
|
25
|
+
}, 200);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Model {
|
|
29
|
+
@Name("id")
|
|
30
|
+
_id: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Controller("/sse")
|
|
34
|
+
export class MyCtrl {
|
|
35
|
+
@Get("/scenario1")
|
|
36
|
+
@EventStream()
|
|
37
|
+
scenario1(@EventStreamCtx() eventStream: EventStreamCtx) {
|
|
38
|
+
make(eventStream, () => new Date());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Get("/scenario2")
|
|
42
|
+
@EventStream()
|
|
43
|
+
scenario2(@EventStreamCtx() eventStream: EventStreamCtx) {
|
|
44
|
+
make(eventStream, () => false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Get("/scenario3")
|
|
48
|
+
@EventStream()
|
|
49
|
+
scenario3(@EventStreamCtx() eventStream: EventStreamCtx) {
|
|
50
|
+
make(eventStream, () => 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Get("/scenario4")
|
|
54
|
+
@EventStream()
|
|
55
|
+
scenario4(@EventStreamCtx() eventStream: EventStreamCtx) {
|
|
56
|
+
make(eventStream, () => ({
|
|
57
|
+
date: new Date(),
|
|
58
|
+
message: "Hello"
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Get("/scenario5")
|
|
63
|
+
@EventStream()
|
|
64
|
+
@(Returns(200).Type(Model))
|
|
65
|
+
scenario5(@EventStreamCtx() eventStream: EventStreamCtx, @Context() $ctx: PlatformContext) {
|
|
66
|
+
make(eventStream, () => {
|
|
67
|
+
const model = new Model();
|
|
68
|
+
model._id = new Date().toISOString();
|
|
69
|
+
|
|
70
|
+
return model;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getControllerFixture(scenario: string) {
|
|
76
|
+
const controller = await PlatformTest.invoke<MyCtrl>(MyCtrl);
|
|
77
|
+
|
|
78
|
+
const endpoint = JsonEntityStore.fromMethod(MyCtrl, scenario);
|
|
79
|
+
const $ctx = PlatformTest.createRequestContext({
|
|
80
|
+
endpoint
|
|
81
|
+
});
|
|
82
|
+
const eventStream = new EventStreamContext({
|
|
83
|
+
$ctx
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {controller, endpoint, eventStream, $ctx};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("SSE integration", () => {
|
|
90
|
+
beforeEach(() => PlatformTest.create());
|
|
91
|
+
afterEach(() => PlatformTest.reset());
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
vi.useFakeTimers({
|
|
94
|
+
toFake: ["Date"],
|
|
95
|
+
now: new Date("2025-01-01T00:00:00Z").getTime()
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
vi.useRealTimers();
|
|
100
|
+
});
|
|
101
|
+
it("should generate the swagger spec", () => {
|
|
102
|
+
expect(getSpec(MyCtrl)).toMatchSnapshot();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("Scenario1 - Date/String", () => {
|
|
106
|
+
it("should pass the new EventStreamContext to function and process message", async () => {
|
|
107
|
+
const {controller, eventStream} = await getControllerFixture("scenario1");
|
|
108
|
+
|
|
109
|
+
controller.scenario1(eventStream);
|
|
110
|
+
|
|
111
|
+
// Wait for the first message
|
|
112
|
+
const result = await new Promise((resolve) => {
|
|
113
|
+
eventStream.on("data", resolve);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual("event: event\ndata: " + JSON.stringify(new Date()) + "\n\n");
|
|
117
|
+
|
|
118
|
+
// stop the interval
|
|
119
|
+
eventStream.end();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("Scenario2 - Boolean", () => {
|
|
124
|
+
it("should pass the new EventStreamContext to function and process message", async () => {
|
|
125
|
+
const {controller, eventStream} = await getControllerFixture("scenario2");
|
|
126
|
+
|
|
127
|
+
controller.scenario2(eventStream);
|
|
128
|
+
|
|
129
|
+
// Wait for the first message
|
|
130
|
+
const result = await new Promise((resolve) => {
|
|
131
|
+
eventStream.on("data", resolve);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual("event: event\ndata: false\n\n");
|
|
135
|
+
|
|
136
|
+
// stop the interval
|
|
137
|
+
eventStream.end();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("Scenario3 - Number", () => {
|
|
142
|
+
it("should pass the new EventStreamContext to function and process message", async () => {
|
|
143
|
+
const {controller, eventStream} = await getControllerFixture("scenario3");
|
|
144
|
+
|
|
145
|
+
controller.scenario3(eventStream);
|
|
146
|
+
|
|
147
|
+
// Wait for the first message
|
|
148
|
+
const result = await new Promise((resolve) => {
|
|
149
|
+
eventStream.on("data", resolve);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result).toEqual("event: event\ndata: 0\n\n");
|
|
153
|
+
|
|
154
|
+
// stop the interval
|
|
155
|
+
eventStream.end();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("Scenario4 - Object", () => {
|
|
160
|
+
it("should pass the new EventStreamContext to function and process message", async () => {
|
|
161
|
+
const {controller, eventStream} = await getControllerFixture("scenario4");
|
|
162
|
+
|
|
163
|
+
controller.scenario4(eventStream);
|
|
164
|
+
|
|
165
|
+
// Wait for the first message
|
|
166
|
+
const result = await new Promise((resolve) => {
|
|
167
|
+
eventStream.on("data", resolve);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result).toEqual(
|
|
171
|
+
"event: event\ndata: " +
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
date: "2025-01-01T00:00:00.000Z",
|
|
174
|
+
message: "Hello"
|
|
175
|
+
}) +
|
|
176
|
+
"\n\n"
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// stop the interval
|
|
180
|
+
eventStream.end();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Scenario5 - Model", () => {
|
|
185
|
+
it("should pass the new EventStreamContext to function and process message", async () => {
|
|
186
|
+
const {controller, eventStream, $ctx} = await getControllerFixture("scenario5");
|
|
187
|
+
|
|
188
|
+
controller.scenario5(eventStream, $ctx);
|
|
189
|
+
|
|
190
|
+
// Wait for the first message
|
|
191
|
+
const result = await new Promise((resolve) => {
|
|
192
|
+
eventStream.on("data", resolve);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual(
|
|
196
|
+
"event: event\ndata: " +
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
id: "2025-01-01T00:00:00.000Z"
|
|
199
|
+
}) +
|
|
200
|
+
"\n\n"
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// stop the interval
|
|
204
|
+
eventStream.end();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|