@j3r3mcdev/oast-server 1.1.6 → 1.1.8
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/.github/workflows/ci.yml +29 -29
- package/.github/workflows/publish.yml +31 -31
- package/README.md +192 -192
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -1
- package/jest.config.js +14 -14
- package/package.json +45 -45
- package/sadmin list shadows +9 -9
- package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -74
- package/src/api/controllers/events.controller.ts +10 -10
- package/src/api/controllers/health.controller.ts +7 -7
- package/src/api/controllers/tasks.controller.ts +41 -41
- package/src/api/dto/__tests__/create-task.dto.test.ts +41 -41
- package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -35
- package/src/api/dto/create-task.dto.ts +33 -33
- package/src/api/dto/filter-tasks.dto.ts +33 -33
- package/src/api/services/__tests__/events.service.test.ts +41 -41
- package/src/api/services/__tests__/tasks.service.test.ts +41 -41
- package/src/api/services/events.service.ts +17 -17
- package/src/api/services/tasks.service.ts +79 -79
- package/src/api/sse/events.stream.ts +90 -90
- package/src/bootstrap.ts +89 -89
- package/src/core/__tests__/core-router.test.ts +30 -30
- package/src/core/__tests__/core-server.test.ts +44 -44
- package/src/core/__tests__/event.normalizer.test.ts +56 -56
- package/src/core/__tests__/event.router.test.ts +89 -89
- package/src/core/__tests__/logger.test.ts +32 -32
- package/src/core/__tests__/storage-manager.test.ts +74 -74
- package/src/core/event.normalizer.ts +147 -147
- package/src/core/event.router.ts +13 -13
- package/src/core/http/__tests__/adapter-node.test.ts +52 -52
- package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -41
- package/src/core/http/__tests__/body-parser-raw.test.ts +28 -28
- package/src/core/http/__tests__/body-parser-text.test.ts +28 -28
- package/src/core/http/__tests__/compile-path.test.ts +39 -39
- package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -51
- package/src/core/http/__tests__/request.test.ts +34 -34
- package/src/core/http/__tests__/response.test.ts +35 -35
- package/src/core/http/__tests__/router-match.test.ts +171 -171
- package/src/core/http/adapter-node.ts +51 -51
- package/src/core/http/buildRequest.ts +18 -18
- package/src/core/http/compile-path.ts +32 -32
- package/src/core/http/errors.ts +37 -37
- package/src/core/http/http-server.ts +52 -52
- package/src/core/http/middleware.ts +160 -160
- package/src/core/http/request.ts +55 -55
- package/src/core/http/response.ts +93 -93
- package/src/core/http/router.ts +138 -138
- package/src/core/id-generator.ts +8 -8
- package/src/core/logger.ts +113 -113
- package/src/core/router.ts +44 -44
- package/src/core/server.ts +85 -85
- package/src/core/storage.ts +64 -64
- package/src/index.ts +14 -14
- package/src/listeners/api/__tests__/api.controller.test.ts +116 -116
- package/src/listeners/api/__tests__/api.extractor.test.ts +46 -46
- package/src/listeners/api/__tests__/api.listener.test.ts +82 -82
- package/src/listeners/api/__tests__/api.routes.test.ts +155 -155
- package/src/listeners/api/__tests__/api.sse.test.ts +105 -105
- package/src/listeners/api/api.controllers.ts +67 -67
- package/src/listeners/api/api.extractor.ts +43 -43
- package/src/listeners/api/api.listener.ts +50 -50
- package/src/listeners/api/api.routes.ts +76 -76
- package/src/listeners/api/api.sse.ts +38 -38
- package/src/listeners/dns/__tests__/dns.test.ts +118 -118
- package/src/listeners/dns/dns.extractor.ts +14 -14
- package/src/listeners/dns/dns.listener.ts +61 -61
- package/src/listeners/http/__tests__/http.extractor.test.ts +59 -59
- package/src/listeners/http/__tests__/http.listener.test.ts +133 -133
- package/src/listeners/http/http.extractor.ts +15 -15
- package/src/listeners/http/http.listener.ts +110 -110
- package/src/listeners/listener.interface.ts +4 -4
- package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -69
- package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -150
- package/src/listeners/smtp/smtp.extractor.ts +18 -18
- package/src/listeners/smtp/smtp.listener.ts +60 -60
- package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -41
- package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +87 -87
- package/src/listeners/ssrf/ssrf.extractor.ts +14 -14
- package/src/listeners/ssrf/ssrf.listener.ts +37 -37
- package/src/listeners/tcp/tcp.extractor.ts +16 -16
- package/src/listeners/tcp/tcp.listener.ts +61 -61
- package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -35
- package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -122
- package/src/listeners/webhook/webhook.extractor.ts +12 -12
- package/src/listeners/webhook/webhook.listener.ts +58 -58
- package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -33
- package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -90
- package/src/listeners/websocket/websocket.extractor.ts +11 -11
- package/src/listeners/websocket/websocket.listener.ts +40 -40
- package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -75
- package/src/storage-adapters/adapters/memory.storage.ts +64 -64
- package/src/storage-adapters/storage.interface.ts +26 -26
- package/src/types/event.types.ts +147 -147
- package/tsconfig.json +20 -21
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
export interface FilterTasksDto {
|
|
2
|
-
status?: "pending" | "running" | "completed" | "failed";
|
|
3
|
-
limit?: number;
|
|
4
|
-
type?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class FilterTasksDtoValidator {
|
|
8
|
-
static validate(query: any): FilterTasksDto {
|
|
9
|
-
const dto: FilterTasksDto = {};
|
|
10
|
-
|
|
11
|
-
if (query.status) {
|
|
12
|
-
const allowed = ["pending", "running", "completed", "failed"];
|
|
13
|
-
if (!allowed.includes(query.status)) {
|
|
14
|
-
throw new Error("Invalid status");
|
|
15
|
-
}
|
|
16
|
-
dto.status = query.status;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (query.limit) {
|
|
20
|
-
const n = Number(query.limit);
|
|
21
|
-
if (isNaN(n) || n <= 0) {
|
|
22
|
-
throw new Error("Invalid limit");
|
|
23
|
-
}
|
|
24
|
-
dto.limit = n;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (query.type) {
|
|
28
|
-
dto.type = String(query.type);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return dto;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
export interface FilterTasksDto {
|
|
2
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
3
|
+
limit?: number;
|
|
4
|
+
type?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class FilterTasksDtoValidator {
|
|
8
|
+
static validate(query: any): FilterTasksDto {
|
|
9
|
+
const dto: FilterTasksDto = {};
|
|
10
|
+
|
|
11
|
+
if (query.status) {
|
|
12
|
+
const allowed = ["pending", "running", "completed", "failed"];
|
|
13
|
+
if (!allowed.includes(query.status)) {
|
|
14
|
+
throw new Error("Invalid status");
|
|
15
|
+
}
|
|
16
|
+
dto.status = query.status;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (query.limit) {
|
|
20
|
+
const n = Number(query.limit);
|
|
21
|
+
if (isNaN(n) || n <= 0) {
|
|
22
|
+
throw new Error("Invalid limit");
|
|
23
|
+
}
|
|
24
|
+
dto.limit = n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (query.type) {
|
|
28
|
+
dto.type = String(query.type);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return dto;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { describe, it, expect, jest } from "@jest/globals";
|
|
2
|
-
import { TasksService } from "../tasks.service";
|
|
3
|
-
import { EventsService } from "../events.service";
|
|
4
|
-
import { EventStreamManager } from "../../sse/events.stream";
|
|
5
|
-
|
|
6
|
-
describe("TasksService", () => {
|
|
7
|
-
it("émet un événement lors de la création d'une tâche", () => {
|
|
8
|
-
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
9
|
-
const events = new EventsService(stream);
|
|
10
|
-
|
|
11
|
-
const spy = jest.spyOn(stream, "broadcast");
|
|
12
|
-
|
|
13
|
-
const service = new TasksService(events);
|
|
14
|
-
|
|
15
|
-
const task = service.create({ type: "x", payload: {} });
|
|
16
|
-
|
|
17
|
-
expect(spy).toHaveBeenCalledWith(
|
|
18
|
-
"tasks",
|
|
19
|
-
"task.created",
|
|
20
|
-
expect.objectContaining({ id: task.id }),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("émet un événement lors de l'annulation d'une tâche", () => {
|
|
25
|
-
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
26
|
-
const events = new EventsService(stream);
|
|
27
|
-
|
|
28
|
-
const spy = jest.spyOn(stream, "broadcast");
|
|
29
|
-
|
|
30
|
-
const service = new TasksService(events);
|
|
31
|
-
|
|
32
|
-
const task = service.create({ type: "x", payload: {} });
|
|
33
|
-
service.cancel(task.id);
|
|
34
|
-
|
|
35
|
-
expect(spy).toHaveBeenCalledWith(
|
|
36
|
-
"tasks",
|
|
37
|
-
"task.cancelled",
|
|
38
|
-
expect.objectContaining({ id: task.id }),
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
|
2
|
+
import { TasksService } from "../tasks.service";
|
|
3
|
+
import { EventsService } from "../events.service";
|
|
4
|
+
import { EventStreamManager } from "../../sse/events.stream";
|
|
5
|
+
|
|
6
|
+
describe("TasksService", () => {
|
|
7
|
+
it("émet un événement lors de la création d'une tâche", () => {
|
|
8
|
+
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
9
|
+
const events = new EventsService(stream);
|
|
10
|
+
|
|
11
|
+
const spy = jest.spyOn(stream, "broadcast");
|
|
12
|
+
|
|
13
|
+
const service = new TasksService(events);
|
|
14
|
+
|
|
15
|
+
const task = service.create({ type: "x", payload: {} });
|
|
16
|
+
|
|
17
|
+
expect(spy).toHaveBeenCalledWith(
|
|
18
|
+
"tasks",
|
|
19
|
+
"task.created",
|
|
20
|
+
expect.objectContaining({ id: task.id }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("émet un événement lors de l'annulation d'une tâche", () => {
|
|
25
|
+
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
26
|
+
const events = new EventsService(stream);
|
|
27
|
+
|
|
28
|
+
const spy = jest.spyOn(stream, "broadcast");
|
|
29
|
+
|
|
30
|
+
const service = new TasksService(events);
|
|
31
|
+
|
|
32
|
+
const task = service.create({ type: "x", payload: {} });
|
|
33
|
+
service.cancel(task.id);
|
|
34
|
+
|
|
35
|
+
expect(spy).toHaveBeenCalledWith(
|
|
36
|
+
"tasks",
|
|
37
|
+
"task.cancelled",
|
|
38
|
+
expect.objectContaining({ id: task.id }),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { describe, it, expect, jest } from "@jest/globals";
|
|
2
|
-
import { TasksService } from "../tasks.service";
|
|
3
|
-
import { EventsService } from "../events.service";
|
|
4
|
-
import { EventStreamManager } from "../../sse/events.stream";
|
|
5
|
-
|
|
6
|
-
describe("TasksService", () => {
|
|
7
|
-
it("émet un événement lors de la création d'une tâche", () => {
|
|
8
|
-
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
9
|
-
const events = new EventsService(stream);
|
|
10
|
-
|
|
11
|
-
const spy = jest.spyOn(stream, "broadcast");
|
|
12
|
-
|
|
13
|
-
const service = new TasksService(events);
|
|
14
|
-
|
|
15
|
-
const task = service.create({ type: "x", payload: {} });
|
|
16
|
-
|
|
17
|
-
expect(spy).toHaveBeenCalledWith(
|
|
18
|
-
"tasks",
|
|
19
|
-
"task.created",
|
|
20
|
-
expect.objectContaining({ id: task.id }),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("émet un événement lors de l'annulation d'une tâche", () => {
|
|
25
|
-
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
26
|
-
const events = new EventsService(stream);
|
|
27
|
-
|
|
28
|
-
const spy = jest.spyOn(stream, "broadcast");
|
|
29
|
-
|
|
30
|
-
const service = new TasksService(events);
|
|
31
|
-
|
|
32
|
-
const task = service.create({ type: "x", payload: {} });
|
|
33
|
-
service.cancel(task.id);
|
|
34
|
-
|
|
35
|
-
expect(spy).toHaveBeenCalledWith(
|
|
36
|
-
"tasks",
|
|
37
|
-
"task.cancelled",
|
|
38
|
-
expect.objectContaining({ id: task.id }),
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
|
2
|
+
import { TasksService } from "../tasks.service";
|
|
3
|
+
import { EventsService } from "../events.service";
|
|
4
|
+
import { EventStreamManager } from "../../sse/events.stream";
|
|
5
|
+
|
|
6
|
+
describe("TasksService", () => {
|
|
7
|
+
it("émet un événement lors de la création d'une tâche", () => {
|
|
8
|
+
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
9
|
+
const events = new EventsService(stream);
|
|
10
|
+
|
|
11
|
+
const spy = jest.spyOn(stream, "broadcast");
|
|
12
|
+
|
|
13
|
+
const service = new TasksService(events);
|
|
14
|
+
|
|
15
|
+
const task = service.create({ type: "x", payload: {} });
|
|
16
|
+
|
|
17
|
+
expect(spy).toHaveBeenCalledWith(
|
|
18
|
+
"tasks",
|
|
19
|
+
"task.created",
|
|
20
|
+
expect.objectContaining({ id: task.id }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("émet un événement lors de l'annulation d'une tâche", () => {
|
|
25
|
+
const stream = new EventStreamManager({ heartbeatInterval: 999999 });
|
|
26
|
+
const events = new EventsService(stream);
|
|
27
|
+
|
|
28
|
+
const spy = jest.spyOn(stream, "broadcast");
|
|
29
|
+
|
|
30
|
+
const service = new TasksService(events);
|
|
31
|
+
|
|
32
|
+
const task = service.create({ type: "x", payload: {} });
|
|
33
|
+
service.cancel(task.id);
|
|
34
|
+
|
|
35
|
+
expect(spy).toHaveBeenCalledWith(
|
|
36
|
+
"tasks",
|
|
37
|
+
"task.cancelled",
|
|
38
|
+
expect.objectContaining({ id: task.id }),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { EventStreamManager } from "../sse/events.stream";
|
|
2
|
-
|
|
3
|
-
export class EventsService {
|
|
4
|
-
constructor(private stream: EventStreamManager) {}
|
|
5
|
-
|
|
6
|
-
connect(res: any, channels: string[] = []) {
|
|
7
|
-
return this.stream.addClient(res, channels);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
emit(channel: string, event: string, data: any) {
|
|
11
|
-
this.stream.broadcast(channel, event, data);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
emitAll(event: string, data: any) {
|
|
15
|
-
this.stream.broadcastAll(event, data);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
1
|
+
import { EventStreamManager } from "../sse/events.stream";
|
|
2
|
+
|
|
3
|
+
export class EventsService {
|
|
4
|
+
constructor(private stream: EventStreamManager) {}
|
|
5
|
+
|
|
6
|
+
connect(res: any, channels: string[] = []) {
|
|
7
|
+
return this.stream.addClient(res, channels);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
emit(channel: string, event: string, data: any) {
|
|
11
|
+
this.stream.broadcast(channel, event, data);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
emitAll(event: string, data: any) {
|
|
15
|
+
this.stream.broadcastAll(event, data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { CreateTaskDto } from "../dto/create-task.dto";
|
|
2
|
-
import { FilterTasksDto } from "../dto/filter-tasks.dto";
|
|
3
|
-
import { EventsService } from "./events.service";
|
|
4
|
-
|
|
5
|
-
export interface Task {
|
|
6
|
-
id: string;
|
|
7
|
-
type: string;
|
|
8
|
-
payload: Record<string, any>;
|
|
9
|
-
priority: "low" | "normal" | "high";
|
|
10
|
-
metadata: Record<string, any>;
|
|
11
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
12
|
-
createdAt: number;
|
|
13
|
-
updatedAt: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class TasksService {
|
|
17
|
-
private tasks = new Map<string, Task>();
|
|
18
|
-
private events: EventsService;
|
|
19
|
-
|
|
20
|
-
constructor(events: EventsService) {
|
|
21
|
-
this.events = events;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
create(dto: CreateTaskDto): Task {
|
|
25
|
-
const id = crypto.randomUUID();
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
|
|
28
|
-
const task: Task = {
|
|
29
|
-
id,
|
|
30
|
-
type: dto.type,
|
|
31
|
-
payload: dto.payload,
|
|
32
|
-
priority: dto.priority ?? "normal",
|
|
33
|
-
metadata: dto.metadata ?? {},
|
|
34
|
-
status: "pending",
|
|
35
|
-
createdAt: now,
|
|
36
|
-
updatedAt: now,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
this.tasks.set(id, task);
|
|
40
|
-
|
|
41
|
-
this.events.emit("tasks", "task.created", task);
|
|
42
|
-
|
|
43
|
-
return task;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get(id: string): Task | undefined {
|
|
47
|
-
return this.tasks.get(id);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
list(filters: FilterTasksDto): Task[] {
|
|
51
|
-
let results = Array.from(this.tasks.values());
|
|
52
|
-
|
|
53
|
-
if (filters.status) {
|
|
54
|
-
results = results.filter((t) => t.status === filters.status);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (filters.type) {
|
|
58
|
-
results = results.filter((t) => t.type === filters.type);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (filters.limit) {
|
|
62
|
-
results = results.slice(0, filters.limit);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return results;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
cancel(id: string): boolean {
|
|
69
|
-
const task = this.tasks.get(id);
|
|
70
|
-
if (!task) return false;
|
|
71
|
-
|
|
72
|
-
task.status = "failed";
|
|
73
|
-
task.updatedAt = Date.now();
|
|
74
|
-
|
|
75
|
-
this.events.emit("tasks", "task.cancelled", task);
|
|
76
|
-
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import { CreateTaskDto } from "../dto/create-task.dto";
|
|
2
|
+
import { FilterTasksDto } from "../dto/filter-tasks.dto";
|
|
3
|
+
import { EventsService } from "./events.service";
|
|
4
|
+
|
|
5
|
+
export interface Task {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
payload: Record<string, any>;
|
|
9
|
+
priority: "low" | "normal" | "high";
|
|
10
|
+
metadata: Record<string, any>;
|
|
11
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
12
|
+
createdAt: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TasksService {
|
|
17
|
+
private tasks = new Map<string, Task>();
|
|
18
|
+
private events: EventsService;
|
|
19
|
+
|
|
20
|
+
constructor(events: EventsService) {
|
|
21
|
+
this.events = events;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
create(dto: CreateTaskDto): Task {
|
|
25
|
+
const id = crypto.randomUUID();
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
|
|
28
|
+
const task: Task = {
|
|
29
|
+
id,
|
|
30
|
+
type: dto.type,
|
|
31
|
+
payload: dto.payload,
|
|
32
|
+
priority: dto.priority ?? "normal",
|
|
33
|
+
metadata: dto.metadata ?? {},
|
|
34
|
+
status: "pending",
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.tasks.set(id, task);
|
|
40
|
+
|
|
41
|
+
this.events.emit("tasks", "task.created", task);
|
|
42
|
+
|
|
43
|
+
return task;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get(id: string): Task | undefined {
|
|
47
|
+
return this.tasks.get(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
list(filters: FilterTasksDto): Task[] {
|
|
51
|
+
let results = Array.from(this.tasks.values());
|
|
52
|
+
|
|
53
|
+
if (filters.status) {
|
|
54
|
+
results = results.filter((t) => t.status === filters.status);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (filters.type) {
|
|
58
|
+
results = results.filter((t) => t.type === filters.type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (filters.limit) {
|
|
62
|
+
results = results.slice(0, filters.limit);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cancel(id: string): boolean {
|
|
69
|
+
const task = this.tasks.get(id);
|
|
70
|
+
if (!task) return false;
|
|
71
|
+
|
|
72
|
+
task.status = "failed";
|
|
73
|
+
task.updatedAt = Date.now();
|
|
74
|
+
|
|
75
|
+
this.events.emit("tasks", "task.cancelled", task);
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
// src/api/sse/event-stream.ts
|
|
2
|
-
|
|
3
|
-
export interface EventStreamConfig {
|
|
4
|
-
retry?: number;
|
|
5
|
-
heartbeatInterval?: number;
|
|
6
|
-
logger?: { info: Function; error: Function };
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface StreamClient {
|
|
10
|
-
id: string;
|
|
11
|
-
res: any;
|
|
12
|
-
channels: Set<string>;
|
|
13
|
-
connectedAt: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class EventStreamManager {
|
|
17
|
-
private clients = new Map<string, StreamClient>();
|
|
18
|
-
private config: Required<EventStreamConfig>;
|
|
19
|
-
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
20
|
-
|
|
21
|
-
constructor(config: EventStreamConfig = {}) {
|
|
22
|
-
this.config = {
|
|
23
|
-
retry: config.retry ?? 2000,
|
|
24
|
-
heartbeatInterval: config.heartbeatInterval ?? 15000,
|
|
25
|
-
logger: config.logger ?? console,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
this.startHeartbeat();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private startHeartbeat() {
|
|
32
|
-
if (process.env.NODE_ENV === "test") return;
|
|
33
|
-
|
|
34
|
-
this.heartbeatTimer = setInterval(() => {
|
|
35
|
-
for (const client of this.clients.values()) {
|
|
36
|
-
client.res.write(`: heartbeat\n\n`);
|
|
37
|
-
}
|
|
38
|
-
}, this.config.heartbeatInterval);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
addClient(res: any, channels: string[] = []): string {
|
|
42
|
-
const id = crypto.randomUUID();
|
|
43
|
-
|
|
44
|
-
const client: StreamClient = {
|
|
45
|
-
id,
|
|
46
|
-
res,
|
|
47
|
-
channels: new Set(channels),
|
|
48
|
-
connectedAt: Date.now(),
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
this.clients.set(id, client);
|
|
52
|
-
|
|
53
|
-
res.write(`retry: ${this.config.retry}\n`);
|
|
54
|
-
res.write(`event: connected\ndata: {"id":"${id}"}\n\n`);
|
|
55
|
-
|
|
56
|
-
res.on("close", () => this.removeClient(id));
|
|
57
|
-
|
|
58
|
-
this.config.logger.info(`SSE client connected: ${id}`);
|
|
59
|
-
|
|
60
|
-
return id;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
removeClient(id: string) {
|
|
64
|
-
if (this.clients.has(id)) {
|
|
65
|
-
this.clients.delete(id);
|
|
66
|
-
this.config.logger.info(`SSE client disconnected: ${id}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
broadcast(channel: string, event: string, data: any) {
|
|
71
|
-
const payload =
|
|
72
|
-
`event: ${event}\n` +
|
|
73
|
-
`data: ${JSON.stringify(data)}\n` +
|
|
74
|
-
`channel: ${channel}\n\n`;
|
|
75
|
-
|
|
76
|
-
for (const client of this.clients.values()) {
|
|
77
|
-
if (client.channels.has(channel)) {
|
|
78
|
-
client.res.write(payload);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
broadcastAll(event: string, data: any) {
|
|
84
|
-
const payload = `event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`;
|
|
85
|
-
|
|
86
|
-
for (const client of this.clients.values()) {
|
|
87
|
-
client.res.write(payload);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
1
|
+
// src/api/sse/event-stream.ts
|
|
2
|
+
|
|
3
|
+
export interface EventStreamConfig {
|
|
4
|
+
retry?: number;
|
|
5
|
+
heartbeatInterval?: number;
|
|
6
|
+
logger?: { info: Function; error: Function };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StreamClient {
|
|
10
|
+
id: string;
|
|
11
|
+
res: any;
|
|
12
|
+
channels: Set<string>;
|
|
13
|
+
connectedAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class EventStreamManager {
|
|
17
|
+
private clients = new Map<string, StreamClient>();
|
|
18
|
+
private config: Required<EventStreamConfig>;
|
|
19
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(config: EventStreamConfig = {}) {
|
|
22
|
+
this.config = {
|
|
23
|
+
retry: config.retry ?? 2000,
|
|
24
|
+
heartbeatInterval: config.heartbeatInterval ?? 15000,
|
|
25
|
+
logger: config.logger ?? console,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.startHeartbeat();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private startHeartbeat() {
|
|
32
|
+
if (process.env.NODE_ENV === "test") return;
|
|
33
|
+
|
|
34
|
+
this.heartbeatTimer = setInterval(() => {
|
|
35
|
+
for (const client of this.clients.values()) {
|
|
36
|
+
client.res.write(`: heartbeat\n\n`);
|
|
37
|
+
}
|
|
38
|
+
}, this.config.heartbeatInterval);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addClient(res: any, channels: string[] = []): string {
|
|
42
|
+
const id = crypto.randomUUID();
|
|
43
|
+
|
|
44
|
+
const client: StreamClient = {
|
|
45
|
+
id,
|
|
46
|
+
res,
|
|
47
|
+
channels: new Set(channels),
|
|
48
|
+
connectedAt: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.clients.set(id, client);
|
|
52
|
+
|
|
53
|
+
res.write(`retry: ${this.config.retry}\n`);
|
|
54
|
+
res.write(`event: connected\ndata: {"id":"${id}"}\n\n`);
|
|
55
|
+
|
|
56
|
+
res.on("close", () => this.removeClient(id));
|
|
57
|
+
|
|
58
|
+
this.config.logger.info(`SSE client connected: ${id}`);
|
|
59
|
+
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeClient(id: string) {
|
|
64
|
+
if (this.clients.has(id)) {
|
|
65
|
+
this.clients.delete(id);
|
|
66
|
+
this.config.logger.info(`SSE client disconnected: ${id}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
broadcast(channel: string, event: string, data: any) {
|
|
71
|
+
const payload =
|
|
72
|
+
`event: ${event}\n` +
|
|
73
|
+
`data: ${JSON.stringify(data)}\n` +
|
|
74
|
+
`channel: ${channel}\n\n`;
|
|
75
|
+
|
|
76
|
+
for (const client of this.clients.values()) {
|
|
77
|
+
if (client.channels.has(channel)) {
|
|
78
|
+
client.res.write(payload);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
broadcastAll(event: string, data: any) {
|
|
84
|
+
const payload = `event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`;
|
|
85
|
+
|
|
86
|
+
for (const client of this.clients.values()) {
|
|
87
|
+
client.res.write(payload);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|