@m6d/cortex-server 1.0.0
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/README.md +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/src/adapters/database.d.ts +27 -0
- package/dist/src/adapters/minio.d.ts +10 -0
- package/dist/src/adapters/mssql.d.ts +3 -0
- package/dist/src/adapters/storage.d.ts +6 -0
- package/dist/src/ai/fetch.d.ts +2 -0
- package/dist/src/ai/helpers.d.ts +5 -0
- package/dist/src/ai/index.d.ts +4 -0
- package/dist/src/ai/interceptors/resolve-captured-files.d.ts +11 -0
- package/dist/src/ai/prompt.d.ts +4 -0
- package/dist/src/ai/tools/call-endpoint.tool.d.ts +7 -0
- package/dist/src/ai/tools/capture-files.tool.d.ts +6 -0
- package/dist/src/ai/tools/execute-code.tool.d.ts +4 -0
- package/dist/src/ai/tools/query-graph.tool.d.ts +5 -0
- package/dist/src/auth/middleware.d.ts +4 -0
- package/dist/src/cli/extract-endpoints.d.ts +6 -0
- package/dist/src/config.d.ts +145 -0
- package/dist/src/db/migrate.d.ts +1 -0
- package/dist/src/db/schema.d.ts +345 -0
- package/dist/src/factory.d.ts +17 -0
- package/dist/src/graph/generate-cypher.d.ts +22 -0
- package/dist/src/graph/helpers.d.ts +60 -0
- package/dist/src/graph/index.d.ts +11 -0
- package/dist/src/graph/neo4j.d.ts +18 -0
- package/dist/src/graph/resolver.d.ts +51 -0
- package/dist/src/graph/seed.d.ts +19 -0
- package/dist/src/graph/types.d.ts +104 -0
- package/dist/src/graph/validate.d.ts +2 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/routes/chat.d.ts +3 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/index.d.ts +4 -0
- package/dist/src/routes/threads.d.ts +3 -0
- package/dist/src/routes/ws.d.ts +3 -0
- package/dist/src/types.d.ts +56 -0
- package/dist/src/ws/connections.d.ts +4 -0
- package/dist/src/ws/events.d.ts +8 -0
- package/dist/src/ws/index.d.ts +3 -0
- package/dist/src/ws/notify.d.ts +2 -0
- package/index.ts +1 -0
- package/package.json +57 -0
- package/src/adapters/database.ts +33 -0
- package/src/adapters/minio.ts +89 -0
- package/src/adapters/mssql.ts +203 -0
- package/src/adapters/storage.ts +6 -0
- package/src/ai/fetch.ts +39 -0
- package/src/ai/helpers.ts +36 -0
- package/src/ai/index.ts +145 -0
- package/src/ai/interceptors/resolve-captured-files.ts +64 -0
- package/src/ai/prompt.ts +120 -0
- package/src/ai/tools/call-endpoint.tool.ts +96 -0
- package/src/ai/tools/capture-files.tool.ts +22 -0
- package/src/ai/tools/execute-code.tool.ts +108 -0
- package/src/ai/tools/query-graph.tool.ts +35 -0
- package/src/auth/middleware.ts +63 -0
- package/src/cli/extract-endpoints.ts +588 -0
- package/src/config.ts +155 -0
- package/src/db/migrate.ts +21 -0
- package/src/db/migrations/20260309012148_cloudy_maria_hill/migration.sql +36 -0
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +305 -0
- package/src/db/schema.ts +77 -0
- package/src/factory.ts +159 -0
- package/src/graph/generate-cypher.ts +179 -0
- package/src/graph/helpers.ts +68 -0
- package/src/graph/index.ts +47 -0
- package/src/graph/neo4j.ts +117 -0
- package/src/graph/resolver.ts +357 -0
- package/src/graph/seed.ts +172 -0
- package/src/graph/types.ts +152 -0
- package/src/graph/validate.ts +80 -0
- package/src/index.ts +27 -0
- package/src/routes/chat.ts +38 -0
- package/src/routes/files.ts +105 -0
- package/src/routes/index.ts +4 -0
- package/src/routes/threads.ts +69 -0
- package/src/routes/ws.ts +33 -0
- package/src/types.ts +50 -0
- package/src/ws/connections.ts +23 -0
- package/src/ws/events.ts +6 -0
- package/src/ws/index.ts +7 -0
- package/src/ws/notify.ts +9 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DomainDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function validateDomain(data: DomainDef) {
|
|
4
|
+
const errors: string[] = [];
|
|
5
|
+
|
|
6
|
+
const seenConceptNames = new Set<string>();
|
|
7
|
+
for (const c of data.concepts ?? []) {
|
|
8
|
+
if (seenConceptNames.has(c.name)) {
|
|
9
|
+
errors.push(`Duplicate concept name: "${c.name}"`);
|
|
10
|
+
}
|
|
11
|
+
seenConceptNames.add(c.name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const seenEndpointNames = new Set<string>();
|
|
15
|
+
for (const e of data.endpoints ?? []) {
|
|
16
|
+
if (seenEndpointNames.has(e.name)) {
|
|
17
|
+
errors.push(`Duplicate endpoint name: "${e.name}"`);
|
|
18
|
+
}
|
|
19
|
+
seenEndpointNames.add(e.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const seenServiceNames = new Set<string>();
|
|
23
|
+
for (const s of data.services ?? []) {
|
|
24
|
+
if (seenServiceNames.has(s.name)) {
|
|
25
|
+
errors.push(`Duplicate service name: "${s.name}"`);
|
|
26
|
+
}
|
|
27
|
+
seenServiceNames.add(s.name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const seenRuleNames = new Set<string>();
|
|
31
|
+
for (const r of data.rules ?? []) {
|
|
32
|
+
if (seenRuleNames.has(r.name)) {
|
|
33
|
+
errors.push(`Duplicate rule name: "${r.name}"`);
|
|
34
|
+
}
|
|
35
|
+
seenRuleNames.add(r.name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cross-domain references are allowed (concepts/endpoints/services may
|
|
39
|
+
// be defined in another domain), so we do not enforce local membership.
|
|
40
|
+
|
|
41
|
+
const referencedConcepts = new Set<string>();
|
|
42
|
+
const referencedRules = new Set<string>();
|
|
43
|
+
|
|
44
|
+
for (const ep of data.endpoints ?? []) {
|
|
45
|
+
ep.queries?.forEach((c) => referencedConcepts.add(c.name));
|
|
46
|
+
ep.mutates?.forEach((c) => referencedConcepts.add(c.name));
|
|
47
|
+
ep.returns?.forEach((r) => referencedConcepts.add(r.concept.name));
|
|
48
|
+
ep.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const svc of data.services ?? []) {
|
|
52
|
+
referencedConcepts.add(svc.belongsTo.name);
|
|
53
|
+
svc.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const c of data.concepts ?? []) {
|
|
57
|
+
c.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
58
|
+
if (c.parentConcept) {
|
|
59
|
+
referencedConcepts.add(c.parentConcept.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const c of data.concepts ?? []) {
|
|
64
|
+
if (!referencedConcepts.has(c.name)) {
|
|
65
|
+
errors.push(
|
|
66
|
+
`Concept "${c.name}" is never referenced by any endpoint, or service`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const r of data.rules ?? []) {
|
|
72
|
+
if (!referencedRules.has(r.name)) {
|
|
73
|
+
errors.push(
|
|
74
|
+
`Rule "${r.name}" is never referenced by any endpoint, service, or concept`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return errors;
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Config types
|
|
2
|
+
export type {
|
|
3
|
+
CortexConfig,
|
|
4
|
+
CortexAgentDefinition,
|
|
5
|
+
KnowledgeConfig,
|
|
6
|
+
DatabaseConfig,
|
|
7
|
+
StorageConfig,
|
|
8
|
+
} from "./config";
|
|
9
|
+
|
|
10
|
+
// Types consumers may need
|
|
11
|
+
export type { Thread, AppEnv } from "./types";
|
|
12
|
+
|
|
13
|
+
// Factory — main entry point
|
|
14
|
+
export type { CortexInstance } from "./factory";
|
|
15
|
+
export { createCortex } from "./factory";
|
|
16
|
+
|
|
17
|
+
// Tools (consumers may register custom tools or use built-in ones)
|
|
18
|
+
export { captureFilesTool } from "./ai/tools/capture-files.tool";
|
|
19
|
+
export { createQueryGraphTool } from "./ai/tools/query-graph.tool";
|
|
20
|
+
export { createCallEndpointTool } from "./ai/tools/call-endpoint.tool";
|
|
21
|
+
export { createExecuteCodeTool } from "./ai/tools/execute-code.tool";
|
|
22
|
+
|
|
23
|
+
// Graph (consumers may use independently)
|
|
24
|
+
export * from "./graph/index";
|
|
25
|
+
|
|
26
|
+
// WebSocket event types
|
|
27
|
+
export type { WsEvent, ThreadTitleUpdatedEvent } from "./ws/index";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { zValidator } from "@hono/zod-validator";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
import type { CortexAppEnv } from "../types.ts";
|
|
6
|
+
import { requireAuth } from "../auth/middleware.ts";
|
|
7
|
+
import { stream } from "../ai/index.ts";
|
|
8
|
+
|
|
9
|
+
export function createChatRoutes() {
|
|
10
|
+
const app = new Hono<CortexAppEnv>();
|
|
11
|
+
|
|
12
|
+
app.post(
|
|
13
|
+
"/chat",
|
|
14
|
+
requireAuth,
|
|
15
|
+
zValidator(
|
|
16
|
+
"json",
|
|
17
|
+
z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
messages: z.array(z.unknown()),
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
async function (c) {
|
|
23
|
+
const config = c.get("agentConfig");
|
|
24
|
+
const { id: userId, token } = c.get("user");
|
|
25
|
+
const { messages, id: threadId } = c.req.valid("json");
|
|
26
|
+
|
|
27
|
+
const thread = await config.db.threads.getById(userId, threadId);
|
|
28
|
+
|
|
29
|
+
if (!thread) {
|
|
30
|
+
throw new HTTPException(404, { message: "Not found" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return stream(messages, thread, userId, token, config);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return app;
|
|
38
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { zValidator } from "@hono/zod-validator";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
import { safeValidateUIMessages, type ToolUIPart, type UIMessage } from "ai";
|
|
6
|
+
import type { CortexAppEnv } from "../types.ts";
|
|
7
|
+
import { requireAuth } from "../auth/middleware.ts";
|
|
8
|
+
|
|
9
|
+
export function createFileRoutes() {
|
|
10
|
+
const app = new Hono<CortexAppEnv>();
|
|
11
|
+
|
|
12
|
+
app.get("/files/:id", requireAuth, async function (c) {
|
|
13
|
+
const config = c.get("agentConfig");
|
|
14
|
+
const id = c.req.param("id");
|
|
15
|
+
const { id: userId } = c.get("user");
|
|
16
|
+
const file = await config.db.capturedFiles.getById(id, userId);
|
|
17
|
+
|
|
18
|
+
if (!file) {
|
|
19
|
+
throw new HTTPException(404, { message: "File not found" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fileStream = await config.storage.stream(`captured_files/${id}`);
|
|
23
|
+
return new Response(fileStream, {
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/octet-stream",
|
|
26
|
+
"Content-Disposition": `attachment; filename="${file.name}"`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.post(
|
|
32
|
+
"/files",
|
|
33
|
+
requireAuth,
|
|
34
|
+
zValidator(
|
|
35
|
+
"json",
|
|
36
|
+
z.object({
|
|
37
|
+
toolCallId: z.string(),
|
|
38
|
+
message: z.unknown().transform(async (message, ctx) => {
|
|
39
|
+
const result = await safeValidateUIMessages({
|
|
40
|
+
messages: [message],
|
|
41
|
+
});
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: "custom",
|
|
45
|
+
path: ["message"],
|
|
46
|
+
message: "Invalid message format",
|
|
47
|
+
});
|
|
48
|
+
return z.NEVER;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result.data[0]!;
|
|
52
|
+
}),
|
|
53
|
+
files: z.array(
|
|
54
|
+
z.object({
|
|
55
|
+
id: z.string(),
|
|
56
|
+
file: z.object({
|
|
57
|
+
name: z.string(),
|
|
58
|
+
bytes: z.string(),
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
async function (c) {
|
|
65
|
+
const config = c.get("agentConfig");
|
|
66
|
+
const { message, toolCallId, files } = c.req.valid("json");
|
|
67
|
+
const { id: userId } = c.get("user");
|
|
68
|
+
|
|
69
|
+
function isTool(value: UIMessage["parts"][number]): value is ToolUIPart {
|
|
70
|
+
return value.type.startsWith("tool-");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toolPart = message.parts
|
|
74
|
+
.filter(isTool)
|
|
75
|
+
.find((x) => x.toolCallId === toolCallId);
|
|
76
|
+
|
|
77
|
+
if (!toolPart) {
|
|
78
|
+
throw new HTTPException(423, {
|
|
79
|
+
message: "Invalid tool call id",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await config.db.capturedFiles.create(
|
|
84
|
+
userId,
|
|
85
|
+
message.id,
|
|
86
|
+
toolPart,
|
|
87
|
+
files,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Store file bytes
|
|
91
|
+
await Promise.all(
|
|
92
|
+
result.map(async (r, i) => {
|
|
93
|
+
await config.storage.put(
|
|
94
|
+
`captured_files/${r.uploadId}`,
|
|
95
|
+
files[i]!.file.bytes,
|
|
96
|
+
);
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return c.json(result);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return app;
|
|
105
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { zValidator } from "@hono/zod-validator";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import type { CortexAppEnv } from "../types.ts";
|
|
5
|
+
import { requireAuth } from "../auth/middleware.ts";
|
|
6
|
+
import { generateTitle } from "../ai/index.ts";
|
|
7
|
+
|
|
8
|
+
export function createThreadRoutes() {
|
|
9
|
+
const app = new Hono<CortexAppEnv>();
|
|
10
|
+
|
|
11
|
+
app.get("/threads", requireAuth, async function (c) {
|
|
12
|
+
const config = c.get("agentConfig");
|
|
13
|
+
const agentId = c.get("agentId");
|
|
14
|
+
const threads = await config.db.threads
|
|
15
|
+
.list(c.get("user").id, agentId)
|
|
16
|
+
.then((x) =>
|
|
17
|
+
x.map((y) => ({ id: y.id, title: y.title, createdAt: y.createdAt })),
|
|
18
|
+
);
|
|
19
|
+
return c.json(threads);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.post(
|
|
23
|
+
"/threads",
|
|
24
|
+
requireAuth,
|
|
25
|
+
zValidator("json", z.object({ prompt: z.string() })),
|
|
26
|
+
async function (c) {
|
|
27
|
+
const config = c.get("agentConfig");
|
|
28
|
+
const agentId = c.get("agentId");
|
|
29
|
+
const { prompt } = c.req.valid("json");
|
|
30
|
+
const { id, createdAt, title } = await config.db.threads.create(
|
|
31
|
+
c.get("user").id,
|
|
32
|
+
agentId,
|
|
33
|
+
);
|
|
34
|
+
generateTitle(id, prompt, c.get("user").id, config);
|
|
35
|
+
return c.json({ id, title, createdAt });
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
app.delete("/threads/:threadId", requireAuth, async function (c) {
|
|
40
|
+
const config = c.get("agentConfig");
|
|
41
|
+
const threadId = c.req.param("threadId");
|
|
42
|
+
await config.db.threads.delete(c.get("user").id, threadId);
|
|
43
|
+
return c.body(null, 204);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
app.get("/threads/:threadId/messages", requireAuth, async function (c) {
|
|
47
|
+
const config = c.get("agentConfig");
|
|
48
|
+
const threadId = c.req.param("threadId");
|
|
49
|
+
const messages = await config.db.messages
|
|
50
|
+
.list(c.get("user").id, threadId)
|
|
51
|
+
.then((x) => x.map((y) => y.content));
|
|
52
|
+
return c.json(messages);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.post(
|
|
56
|
+
"/threads/:threadId/session",
|
|
57
|
+
requireAuth,
|
|
58
|
+
zValidator("json", z.object({ session: z.record(z.unknown()) })),
|
|
59
|
+
async function (c) {
|
|
60
|
+
const config = c.get("agentConfig");
|
|
61
|
+
const threadId = c.req.param("threadId");
|
|
62
|
+
const { session } = c.req.valid("json");
|
|
63
|
+
await config.db.threads.updateSession(threadId, session);
|
|
64
|
+
return c.body(null, 204);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return app;
|
|
69
|
+
}
|
package/src/routes/ws.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { upgradeWebSocket } from "hono/bun";
|
|
3
|
+
import type { AppEnv } from "../types.ts";
|
|
4
|
+
import { addConnection, removeConnection } from "../ws/index.ts";
|
|
5
|
+
|
|
6
|
+
export function createWsRoute() {
|
|
7
|
+
const app = new Hono<AppEnv>();
|
|
8
|
+
|
|
9
|
+
app.get(
|
|
10
|
+
"/ws",
|
|
11
|
+
upgradeWebSocket(function (c) {
|
|
12
|
+
const user = c.get("user");
|
|
13
|
+
const userId = user?.id;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
onOpen(_, ws) {
|
|
17
|
+
if (userId) {
|
|
18
|
+
addConnection(userId, ws);
|
|
19
|
+
}
|
|
20
|
+
ws.send("Connected!");
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
onClose(_, ws) {
|
|
24
|
+
if (userId) {
|
|
25
|
+
removeConnection(userId, ws);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return app;
|
|
33
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ToolUIPart, UIMessage } from "ai";
|
|
2
|
+
import type { ResolvedCortexAgentConfig } from "./config";
|
|
3
|
+
|
|
4
|
+
export type Thread = {
|
|
5
|
+
id: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
title: string | null;
|
|
9
|
+
session: Record<string, unknown> | null;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StoredMessage = {
|
|
15
|
+
id: string;
|
|
16
|
+
threadId: string;
|
|
17
|
+
text: string | null;
|
|
18
|
+
content: UIMessage;
|
|
19
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CapturedFileInput = {
|
|
25
|
+
id: string;
|
|
26
|
+
file: { name: string; bytes: string };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AppEnv = {
|
|
30
|
+
Bindings: {};
|
|
31
|
+
Variables: {
|
|
32
|
+
user: { id: string; token: string } | undefined;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type AuthedAppEnv = {
|
|
37
|
+
Bindings: {};
|
|
38
|
+
Variables: {
|
|
39
|
+
user: { id: string; token: string };
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type CortexAppEnv = {
|
|
44
|
+
Bindings: {};
|
|
45
|
+
Variables: {
|
|
46
|
+
user: { id: string; token: string };
|
|
47
|
+
agentConfig: ResolvedCortexAgentConfig;
|
|
48
|
+
agentId: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WSContext } from "hono/ws";
|
|
2
|
+
|
|
3
|
+
const connections = new Map<string, WSContext[]>();
|
|
4
|
+
|
|
5
|
+
export function addConnection(userId: string, ws: WSContext) {
|
|
6
|
+
const sockets = connections.get(userId) ?? [];
|
|
7
|
+
sockets.push(ws);
|
|
8
|
+
connections.set(userId, sockets);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function removeConnection(userId: string, ws: WSContext) {
|
|
12
|
+
let sockets = connections.get(userId) ?? [];
|
|
13
|
+
sockets = sockets.filter((x) => x !== ws);
|
|
14
|
+
if (sockets.length) {
|
|
15
|
+
connections.set(userId, sockets);
|
|
16
|
+
} else {
|
|
17
|
+
connections.delete(userId);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConnections(userId: string) {
|
|
22
|
+
return connections.get(userId) ?? [];
|
|
23
|
+
}
|
package/src/ws/events.ts
ADDED
package/src/ws/index.ts
ADDED
package/src/ws/notify.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getConnections } from "./connections.ts";
|
|
2
|
+
import type { WsEvent } from "./events.ts";
|
|
3
|
+
|
|
4
|
+
export function notify(userId: string, event: WsEvent) {
|
|
5
|
+
const message = JSON.stringify(event);
|
|
6
|
+
for (const ws of getConnections(userId)) {
|
|
7
|
+
ws.send(message);
|
|
8
|
+
}
|
|
9
|
+
}
|