@simplysm/service-server 13.0.99 → 14.0.1
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/dist/auth/auth-token-payload.js +2 -1
- package/dist/auth/auth-token-payload.js.map +1 -6
- package/dist/auth/jwt-manager.js +21 -21
- package/dist/auth/jwt-manager.js.map +1 -6
- package/dist/core/define-service.d.ts +12 -12
- package/dist/core/define-service.d.ts.map +1 -1
- package/dist/core/define-service.js +77 -63
- package/dist/core/define-service.js.map +1 -6
- package/dist/core/service-executor.d.ts.map +1 -1
- package/dist/core/service-executor.js +42 -32
- package/dist/core/service-executor.js.map +1 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -6
- package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
- package/dist/legacy/v1-auto-update-handler.js +42 -35
- package/dist/legacy/v1-auto-update-handler.js.map +1 -6
- package/dist/protocol/protocol-wrapper.d.ts +9 -9
- package/dist/protocol/protocol-wrapper.js +64 -46
- package/dist/protocol/protocol-wrapper.js.map +1 -6
- package/dist/service-server.d.ts +2 -0
- package/dist/service-server.d.ts.map +1 -1
- package/dist/service-server.js +187 -165
- package/dist/service-server.js.map +1 -6
- package/dist/services/auto-update-service.js +35 -34
- package/dist/services/auto-update-service.js.map +1 -6
- package/dist/services/orm-service.js +114 -120
- package/dist/services/orm-service.js.map +1 -6
- package/dist/transport/http/http-request-handler.d.ts.map +1 -1
- package/dist/transport/http/http-request-handler.js +58 -46
- package/dist/transport/http/http-request-handler.js.map +1 -6
- package/dist/transport/http/static-file-handler.js +42 -39
- package/dist/transport/http/static-file-handler.js.map +1 -6
- package/dist/transport/http/upload-handler.d.ts.map +1 -1
- package/dist/transport/http/upload-handler.js +60 -55
- package/dist/transport/http/upload-handler.js.map +1 -6
- package/dist/transport/socket/service-socket.d.ts +13 -13
- package/dist/transport/socket/service-socket.js +132 -108
- package/dist/transport/socket/service-socket.js.map +1 -6
- package/dist/transport/socket/websocket-handler.d.ts +10 -10
- package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
- package/dist/transport/socket/websocket-handler.js +154 -139
- package/dist/transport/socket/websocket-handler.js.map +1 -6
- package/dist/types/server-options.d.ts +1 -1
- package/dist/types/server-options.d.ts.map +1 -1
- package/dist/types/server-options.js +2 -1
- package/dist/types/server-options.js.map +1 -6
- package/dist/utils/config-manager.js +48 -46
- package/dist/utils/config-manager.js.map +1 -6
- package/dist/workers/service-protocol.worker.js +8 -11
- package/dist/workers/service-protocol.worker.js.map +1 -6
- package/package.json +12 -14
- package/src/auth/jwt-manager.ts +2 -2
- package/src/core/define-service.ts +19 -19
- package/src/core/service-executor.ts +23 -17
- package/src/index.ts +10 -12
- package/src/legacy/v1-auto-update-handler.ts +10 -10
- package/src/protocol/protocol-wrapper.ts +16 -16
- package/src/service-server.ts +52 -39
- package/src/services/auto-update-service.ts +1 -1
- package/src/services/orm-service.ts +7 -7
- package/src/transport/http/http-request-handler.ts +16 -10
- package/src/transport/http/static-file-handler.ts +8 -8
- package/src/transport/http/upload-handler.ts +16 -9
- package/src/transport/socket/service-socket.ts +22 -22
- package/src/transport/socket/websocket-handler.ts +59 -60
- package/src/types/server-options.ts +1 -1
- package/src/utils/config-manager.ts +11 -11
- package/README.md +0 -163
- package/dist/services/smtp-client-service.d.ts +0 -8
- package/dist/services/smtp-client-service.d.ts.map +0 -1
- package/dist/services/smtp-client-service.js +0 -46
- package/dist/services/smtp-client-service.js.map +0 -6
- package/docs/auth.md +0 -59
- package/docs/core.md +0 -133
- package/docs/server.md +0 -126
- package/docs/services.md +0 -58
- package/docs/transport.md +0 -164
- package/src/services/smtp-client-service.ts +0 -59
- package/tests/define-service.spec.ts +0 -66
- package/tests/orm-service.spec.ts +0 -83
- package/tests/service-executor.spec.ts +0 -114
|
@@ -15,7 +15,7 @@ import consola from "consola";
|
|
|
15
15
|
|
|
16
16
|
const logger = consola.withTag("service-server:OrmService");
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// 정적 상태는 팩토리 외부에 존재해야 함 (호출 간 공유)
|
|
19
19
|
const socketConns = new WeakMap<ServiceSocket, Map<number, DbConn>>();
|
|
20
20
|
|
|
21
21
|
export const OrmService = defineService(
|
|
@@ -24,7 +24,7 @@ export const OrmService = defineService(
|
|
|
24
24
|
const sock = (): ServiceSocket => {
|
|
25
25
|
const socket = ctx.socket;
|
|
26
26
|
if (socket == null) {
|
|
27
|
-
throw new Error("WebSocket
|
|
27
|
+
throw new Error("WebSocket 연결이 필요합니다. ORM 서비스는 HTTP로 사용할 수 없습니다.");
|
|
28
28
|
}
|
|
29
29
|
return socket;
|
|
30
30
|
};
|
|
@@ -34,7 +34,7 @@ export const OrmService = defineService(
|
|
|
34
34
|
opt.configName
|
|
35
35
|
];
|
|
36
36
|
if (config == null) {
|
|
37
|
-
throw new Error(`ORM
|
|
37
|
+
throw new Error(`ORM 설정을 찾을 수 없습니다: ${opt.configName}`);
|
|
38
38
|
}
|
|
39
39
|
return { ...config, ...opt.config } as DbConnConfig;
|
|
40
40
|
};
|
|
@@ -43,7 +43,7 @@ export const OrmService = defineService(
|
|
|
43
43
|
const myConns = socketConns.get(sock());
|
|
44
44
|
const conn = myConns?.get(connId);
|
|
45
45
|
if (conn == null) {
|
|
46
|
-
throw new Error("
|
|
46
|
+
throw new Error("데이터베이스에 연결되지 않았습니다. (유효하지 않은 연결 ID)");
|
|
47
47
|
}
|
|
48
48
|
return conn;
|
|
49
49
|
};
|
|
@@ -71,7 +71,7 @@ export const OrmService = defineService(
|
|
|
71
71
|
sock().on("close", async () => {
|
|
72
72
|
if (myConns == null) return;
|
|
73
73
|
|
|
74
|
-
logger.debug("
|
|
74
|
+
logger.debug("소켓 종료 감지: 열린 모든 DB 연결을 정리합니다.");
|
|
75
75
|
const conns = Array.from(myConns.values());
|
|
76
76
|
|
|
77
77
|
await Promise.all(
|
|
@@ -81,7 +81,7 @@ export const OrmService = defineService(
|
|
|
81
81
|
await conn.close();
|
|
82
82
|
}
|
|
83
83
|
} catch (err) {
|
|
84
|
-
logger.warn("
|
|
84
|
+
logger.warn("강제 DB 연결 종료 중 에러 무시됨", err);
|
|
85
85
|
}
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
@@ -110,7 +110,7 @@ export const OrmService = defineService(
|
|
|
110
110
|
const conn = getConn(connId);
|
|
111
111
|
await conn.close();
|
|
112
112
|
} catch (err) {
|
|
113
|
-
logger.warn("
|
|
113
|
+
logger.warn("DB 연결 종료 중 에러 무시됨", err);
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
|
|
@@ -16,49 +16,55 @@ export async function handleHttpRequest<TAuthInfo = unknown>(
|
|
|
16
16
|
): Promise<void> {
|
|
17
17
|
const { service, method } = req.params as { service: string; method: string };
|
|
18
18
|
|
|
19
|
-
// ClientName
|
|
19
|
+
// ClientName 헤더
|
|
20
20
|
const clientName = req.headers["x-sd-client-name"] as string | undefined;
|
|
21
|
-
if (clientName == null) throw new Error("ClientName
|
|
21
|
+
if (clientName == null) throw new Error("ClientName 헤더가 필요합니다");
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Authorization 헤더 파싱 및 검증
|
|
24
24
|
let authTokenPayload: AuthTokenPayload<TAuthInfo> | undefined;
|
|
25
25
|
try {
|
|
26
26
|
const authHeader = req.headers.authorization;
|
|
27
27
|
if (authHeader != null) {
|
|
28
|
-
if (jwtSecret == null) throw new Error("JWT Secret
|
|
28
|
+
if (jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
29
29
|
|
|
30
30
|
const token = authHeader.split(" ")[1]; // "Bearer <token>"
|
|
31
31
|
authTokenPayload = await verifyJwt<TAuthInfo>(jwtSecret, token);
|
|
32
32
|
}
|
|
33
33
|
} catch (err) {
|
|
34
34
|
reply.status(401).send({
|
|
35
|
-
error: "
|
|
35
|
+
error: "인증 실패",
|
|
36
36
|
message: err instanceof Error ? err.message : String(err),
|
|
37
37
|
});
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// 매개변수 파싱
|
|
42
42
|
let params: unknown[] | undefined;
|
|
43
43
|
if (req.method === "GET") {
|
|
44
44
|
const query = req.query as { json?: string };
|
|
45
45
|
if (typeof query.json !== "string") {
|
|
46
|
-
throw new Error("JSON
|
|
46
|
+
throw new Error("JSON 쿼리 파라미터가 필요합니다");
|
|
47
47
|
}
|
|
48
48
|
params = json.parse(query.json);
|
|
49
49
|
} else if (req.method === "POST") {
|
|
50
50
|
if (!Array.isArray(req.body)) {
|
|
51
51
|
reply.status(400).send({
|
|
52
|
-
error: "
|
|
53
|
-
message: "
|
|
52
|
+
error: "잘못된 요청",
|
|
53
|
+
message: "요청 본문은 배열이어야 합니다.",
|
|
54
54
|
});
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
params = req.body as unknown[];
|
|
59
|
+
} else {
|
|
60
|
+
reply.status(405).send({
|
|
61
|
+
error: "Method Not Allowed",
|
|
62
|
+
message: `${req.method} 메서드는 지원하지 않습니다.`,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
//
|
|
67
|
+
// 서비스 실행 및 응답 전송
|
|
62
68
|
if (params != null) {
|
|
63
69
|
const serviceResult = await runMethod({
|
|
64
70
|
serviceName: service,
|
|
@@ -14,12 +14,12 @@ export async function handleStaticFile(
|
|
|
14
14
|
let targetFilePath = path.resolve(rootPath, "www", urlPath);
|
|
15
15
|
const allowedRootPath = path.resolve(rootPath, "www");
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// targetPath 보안 가드 (경로 탐색 공격 방지)
|
|
18
18
|
if (targetFilePath !== allowedRootPath && !pathx.isChildPath(targetFilePath, allowedRootPath)) {
|
|
19
|
-
throw new Error("
|
|
19
|
+
throw new Error("접근이 거부되었습니다");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// 디렉토리에 대해 슬래시를 추가하여 리다이렉트 (표준 웹 서버 동작)
|
|
23
23
|
if ((await fsx.exists(targetFilePath)) && (await fsx.stat(targetFilePath)).isDirectory()) {
|
|
24
24
|
if (!urlPath.endsWith("/")) {
|
|
25
25
|
const urlObj = new URL(req.raw.url!, "http://localhost");
|
|
@@ -29,15 +29,15 @@ export async function handleStaticFile(
|
|
|
29
29
|
targetFilePath = path.resolve(targetFilePath, "index.html");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// 권한 확인 (숨김 파일 등)
|
|
33
33
|
if (path.basename(targetFilePath).startsWith(".")) {
|
|
34
|
-
const errorMessage = "
|
|
34
|
+
const errorMessage = "이 파일에 접근할 권한이 없습니다.";
|
|
35
35
|
responseErrorHtml(reply, 403, errorMessage);
|
|
36
36
|
logger.warn(`[403] ${errorMessage} (${targetFilePath})`);
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// 파일 전송
|
|
41
41
|
const filename = path.basename(targetFilePath);
|
|
42
42
|
const directory = path.dirname(targetFilePath);
|
|
43
43
|
|
|
@@ -46,11 +46,11 @@ export async function handleStaticFile(
|
|
|
46
46
|
} catch (err: unknown) {
|
|
47
47
|
const error = err as { code?: string };
|
|
48
48
|
if (error.code === "ENOENT") {
|
|
49
|
-
const errorMessage = "
|
|
49
|
+
const errorMessage = "파일을 찾을 수 없습니다.";
|
|
50
50
|
responseErrorHtml(reply, 404, errorMessage);
|
|
51
51
|
logger.warn(`[404] ${errorMessage} (${targetFilePath})`);
|
|
52
52
|
} else {
|
|
53
|
-
const errorMessage = "
|
|
53
|
+
const errorMessage = "파일 전송 중 에러가 발생했습니다.";
|
|
54
54
|
responseErrorHtml(reply, 500, errorMessage);
|
|
55
55
|
logger.error(`[500] ${errorMessage}`, err);
|
|
56
56
|
}
|
|
@@ -17,24 +17,24 @@ export async function handleUpload(
|
|
|
17
17
|
jwtSecret: string | undefined,
|
|
18
18
|
): Promise<void> {
|
|
19
19
|
if (!req.isMultipart()) {
|
|
20
|
-
reply.status(400).send("Multipart
|
|
20
|
+
reply.status(400).send("Multipart 요청이 필요합니다");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// 인증 확인
|
|
25
25
|
try {
|
|
26
26
|
const authHeader = req.headers.authorization;
|
|
27
27
|
if (authHeader == null) {
|
|
28
|
-
throw new Error("
|
|
28
|
+
throw new Error("인증 토큰이 누락되었습니다.");
|
|
29
29
|
}
|
|
30
30
|
if (jwtSecret == null) {
|
|
31
|
-
throw new Error("JWT Secret
|
|
31
|
+
throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
32
32
|
}
|
|
33
33
|
const token = authHeader.split(" ")[1];
|
|
34
34
|
await verifyJwt(jwtSecret, token);
|
|
35
35
|
} catch (err) {
|
|
36
36
|
reply.status(401).send({
|
|
37
|
-
error: "
|
|
37
|
+
error: "인증 실패",
|
|
38
38
|
message: err instanceof Error ? err.message : String(err),
|
|
39
39
|
});
|
|
40
40
|
return;
|
|
@@ -58,7 +58,7 @@ export async function handleUpload(
|
|
|
58
58
|
await pipeline(part.file, createWriteStream(currentSavePath));
|
|
59
59
|
|
|
60
60
|
if (part.file.truncated) {
|
|
61
|
-
throw new Error(
|
|
61
|
+
throw new Error(`파일 크기 제한 초과: ${originalFilename}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const stats = await fsx.stat(currentSavePath);
|
|
@@ -75,13 +75,20 @@ export async function handleUpload(
|
|
|
75
75
|
|
|
76
76
|
reply.send(result);
|
|
77
77
|
} catch (err) {
|
|
78
|
-
logger.error("
|
|
78
|
+
logger.error("업로드 에러", err);
|
|
79
79
|
|
|
80
80
|
if (currentSavePath != null) {
|
|
81
81
|
await fsx.rm(currentSavePath).catch(() => {});
|
|
82
|
-
logger.warn(
|
|
82
|
+
logger.warn(`불완전한 파일 삭제됨: ${currentSavePath}`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// 이미 저장된 파일 정리
|
|
86
|
+
for (const savedFile of result) {
|
|
87
|
+
const savedPath = path.resolve(rootPath, "www", savedFile.path);
|
|
88
|
+
await fsx.rm(savedPath).catch(() => {});
|
|
89
|
+
logger.warn(`이미 저장된 파일 삭제됨: ${savedPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
reply.code(500).send("업로드 실패");
|
|
86
93
|
}
|
|
87
94
|
}
|
|
@@ -15,10 +15,10 @@ import type {
|
|
|
15
15
|
const logger = consola.withTag("service-server:ServiceSocket");
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* 서비스 소켓 인터페이스
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* 프로토콜 인코딩/디코딩, ping/pong 연결 유지,
|
|
21
|
+
* 이벤트 리스너 추적이 포함된 단일 WebSocket 연결을 관리한다.
|
|
22
22
|
*/
|
|
23
23
|
export interface ServiceSocket {
|
|
24
24
|
readonly connectedAtDateTime: DateTime;
|
|
@@ -27,37 +27,37 @@ export interface ServiceSocket {
|
|
|
27
27
|
authTokenPayload?: AuthTokenPayload;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* WebSocket 연결을 닫는다
|
|
31
31
|
*/
|
|
32
32
|
close(): void;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* 클라이언트에 메시지를 전송한다
|
|
36
36
|
*/
|
|
37
37
|
send(uuid: string, msg: ServiceServerMessage): Promise<number>;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* key/name/info로 이벤트 리스너를 등록한다
|
|
41
41
|
*/
|
|
42
42
|
addListener(key: string, eventName: string, info: unknown): void;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* key로 이벤트 리스너를 제거한다
|
|
46
46
|
*/
|
|
47
47
|
removeListener(key: string): void;
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* 특정 이벤트 이름에 해당하는 모든 이벤트 리스너를 조회한다
|
|
51
51
|
*/
|
|
52
52
|
getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* 이 소켓의 리스너에 존재하는 대상 키를 필터링한다
|
|
56
56
|
*/
|
|
57
57
|
filterEventTargetKeys(targetKeys: string[]): string[];
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* 이벤트 핸들러를 등록한다
|
|
61
61
|
*/
|
|
62
62
|
on(event: "error", handler: (err: Error) => void): void;
|
|
63
63
|
on(event: "close", handler: (code: number) => void): void;
|
|
@@ -65,10 +65,10 @@ export interface ServiceSocket {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* 서비스 소켓 인스턴스를 생성한다
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* 프로토콜 인코딩/디코딩, ping/pong 연결 유지,
|
|
71
|
+
* 이벤트 리스너 추적이 포함된 단일 WebSocket 연결을 관리한다.
|
|
72
72
|
*/
|
|
73
73
|
export function createServiceSocket(
|
|
74
74
|
socket: WebSocket,
|
|
@@ -77,10 +77,10 @@ export function createServiceSocket(
|
|
|
77
77
|
connReq: FastifyRequest,
|
|
78
78
|
): ServiceSocket {
|
|
79
79
|
// -------------------------------------------------------------------
|
|
80
|
-
//
|
|
80
|
+
// 상태
|
|
81
81
|
// -------------------------------------------------------------------
|
|
82
82
|
|
|
83
|
-
const PING_INTERVAL = 5000; //
|
|
83
|
+
const PING_INTERVAL = 5000; // 5초마다 ping 전송
|
|
84
84
|
const PONG_PACKET = new Uint8Array([0x02]);
|
|
85
85
|
|
|
86
86
|
const protocol = createServerProtocolWrapper();
|
|
@@ -97,7 +97,7 @@ export function createServiceSocket(
|
|
|
97
97
|
const connectedAtDateTime = new DateTime();
|
|
98
98
|
|
|
99
99
|
// -------------------------------------------------------------------
|
|
100
|
-
//
|
|
100
|
+
// 헬퍼
|
|
101
101
|
// -------------------------------------------------------------------
|
|
102
102
|
|
|
103
103
|
async function sendInternal(uuid: string, msg: ServiceServerRawMessage): Promise<number> {
|
|
@@ -121,11 +121,11 @@ export function createServiceSocket(
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// -------------------------------------------------------------------
|
|
124
|
-
//
|
|
124
|
+
// 이벤트 핸들러
|
|
125
125
|
// -------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
function onError(err: Error): void {
|
|
128
|
-
logger.error("WebSocket
|
|
128
|
+
logger.error("WebSocket 클라이언트 에러", err);
|
|
129
129
|
emitEvent("error", err);
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -137,7 +137,7 @@ export function createServiceSocket(
|
|
|
137
137
|
|
|
138
138
|
async function onMessage(msgBuffer: Bytes): Promise<void> {
|
|
139
139
|
try {
|
|
140
|
-
//
|
|
140
|
+
// ping에 대한 pong 응답 처리
|
|
141
141
|
if (msgBuffer.length === 1 && msgBuffer[0] === 0x01) {
|
|
142
142
|
if (socket.readyState === WebSocket.OPEN) {
|
|
143
143
|
socket.send(PONG_PACKET);
|
|
@@ -159,12 +159,12 @@ export function createServiceSocket(
|
|
|
159
159
|
emitEvent("message", { uuid: decodeResult.uuid, msg });
|
|
160
160
|
}
|
|
161
161
|
} catch (err) {
|
|
162
|
-
logger.error("
|
|
162
|
+
logger.error("WebSocket 메시지 처리 중 에러 발생", err);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// -------------------------------------------------------------------
|
|
167
|
-
//
|
|
167
|
+
// 설정
|
|
168
168
|
// -------------------------------------------------------------------
|
|
169
169
|
|
|
170
170
|
socket.on("close", onClose);
|
|
@@ -186,7 +186,7 @@ export function createServiceSocket(
|
|
|
186
186
|
}, PING_INTERVAL);
|
|
187
187
|
|
|
188
188
|
// -------------------------------------------------------------------
|
|
189
|
-
//
|
|
189
|
+
// 공개 API
|
|
190
190
|
// -------------------------------------------------------------------
|
|
191
191
|
|
|
192
192
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WebSocket } from "ws";
|
|
2
|
-
import { Uuid } from "@simplysm/core-common";
|
|
2
|
+
import { Uuid, env } from "@simplysm/core-common";
|
|
3
3
|
import type { ServiceEventDef, ServiceClientMessage } from "@simplysm/service-common";
|
|
4
4
|
import { createServiceSocket, type ServiceSocket } from "./service-socket";
|
|
5
5
|
import { verifyJwt } from "../../auth/jwt-manager";
|
|
@@ -9,29 +9,29 @@ import consola from "consola";
|
|
|
9
9
|
const logger = consola.withTag("service-server:WebSocketHandler");
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* WebSocket
|
|
12
|
+
* WebSocket 핸들러 인터페이스
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 여러 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며,
|
|
15
|
+
* 이벤트 브로드캐스팅을 처리한다.
|
|
16
16
|
*/
|
|
17
17
|
export interface WebSocketHandler {
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* 새 WebSocket 연결을 추가한다
|
|
20
20
|
*/
|
|
21
21
|
addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* 모든 활성 연결을 닫는다
|
|
25
25
|
*/
|
|
26
26
|
closeAll(): void;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* 모든 클라이언트에 리로드 메시지를 브로드캐스트한다
|
|
30
30
|
*/
|
|
31
31
|
broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* 매칭되는 클라이언트에 이벤트를 발생시킨다
|
|
35
35
|
*/
|
|
36
36
|
emit<TInfo, TData>(
|
|
37
37
|
eventDef: ServiceEventDef<TInfo, TData>,
|
|
@@ -41,10 +41,10 @@ export interface WebSocketHandler {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* WebSocket 핸들러 인스턴스를 생성한다
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* 여러 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며,
|
|
47
|
+
* 이벤트 브로드캐스팅을 처리한다.
|
|
48
48
|
*/
|
|
49
49
|
export function createWebSocketHandler(
|
|
50
50
|
runMethod: (def: {
|
|
@@ -56,13 +56,13 @@ export function createWebSocketHandler(
|
|
|
56
56
|
jwtSecret: string | undefined,
|
|
57
57
|
): WebSocketHandler {
|
|
58
58
|
// -------------------------------------------------------------------
|
|
59
|
-
//
|
|
59
|
+
// 상태
|
|
60
60
|
// -------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
const socketMap = new Map<string, ServiceSocket>();
|
|
63
63
|
|
|
64
64
|
// -------------------------------------------------------------------
|
|
65
|
-
//
|
|
65
|
+
// 헬퍼
|
|
66
66
|
// -------------------------------------------------------------------
|
|
67
67
|
|
|
68
68
|
async function processRequest(
|
|
@@ -72,7 +72,9 @@ export function createWebSocketHandler(
|
|
|
72
72
|
): Promise<number> {
|
|
73
73
|
try {
|
|
74
74
|
if (message.name.includes(".") && Array.isArray(message.body)) {
|
|
75
|
-
const
|
|
75
|
+
const dotIndex = message.name.indexOf(".");
|
|
76
|
+
const serviceName = message.name.substring(0, dotIndex);
|
|
77
|
+
const methodName = message.name.substring(dotIndex + 1);
|
|
76
78
|
|
|
77
79
|
const result = await runMethod({
|
|
78
80
|
serviceName,
|
|
@@ -99,35 +101,34 @@ export function createWebSocketHandler(
|
|
|
99
101
|
} else if (message.name === "evt:emit") {
|
|
100
102
|
const { keys, data } = message.body as { keys: string[]; data: unknown };
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
keys: targetKeys,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
104
|
+
await Promise.allSettled(
|
|
105
|
+
Array.from(socketMap.values()).map(async (subSock) => {
|
|
106
|
+
const targetKeys = subSock.filterEventTargetKeys(keys);
|
|
107
|
+
if (targetKeys.length > 0) {
|
|
108
|
+
await subSock.send(uuid, {
|
|
109
|
+
name: "evt:on",
|
|
110
|
+
body: { keys: targetKeys, data },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
114
115
|
|
|
115
116
|
return await serviceSocket.send(uuid, { name: "response" });
|
|
116
117
|
} else if (message.name === "auth") {
|
|
117
|
-
if (jwtSecret == null) throw new Error("JWT Secret
|
|
118
|
+
if (jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
118
119
|
|
|
119
120
|
const token = message.body;
|
|
120
121
|
serviceSocket.authTokenPayload = await verifyJwt(jwtSecret, token);
|
|
121
122
|
return await serviceSocket.send(uuid, { name: "response" });
|
|
122
123
|
} else {
|
|
123
|
-
const err = new Error("
|
|
124
|
+
const err = new Error("유효하지 않은 요청입니다.");
|
|
124
125
|
|
|
125
126
|
return await serviceSocket.send(uuid, {
|
|
126
127
|
name: "error",
|
|
127
128
|
body: {
|
|
128
129
|
name: err.name,
|
|
129
130
|
message: err.message,
|
|
130
|
-
stack: err.stack,
|
|
131
|
+
...(env.DEV ? { stack: err.stack } : {}),
|
|
131
132
|
code: "BAD_MESSAGE",
|
|
132
133
|
},
|
|
133
134
|
});
|
|
@@ -136,7 +137,7 @@ export function createWebSocketHandler(
|
|
|
136
137
|
const error =
|
|
137
138
|
err instanceof Error
|
|
138
139
|
? err
|
|
139
|
-
: new Error(typeof err === "string" ? err : "
|
|
140
|
+
: new Error(typeof err === "string" ? err : "알 수 없는 에러가 발생했습니다.");
|
|
140
141
|
|
|
141
142
|
return serviceSocket.send(uuid, {
|
|
142
143
|
name: "error",
|
|
@@ -144,14 +145,14 @@ export function createWebSocketHandler(
|
|
|
144
145
|
name: error.name,
|
|
145
146
|
message: error.message,
|
|
146
147
|
code: "INTERNAL_ERROR",
|
|
147
|
-
stack: error.stack,
|
|
148
|
+
...(env.DEV ? { stack: error.stack } : {}),
|
|
148
149
|
},
|
|
149
150
|
});
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
// -------------------------------------------------------------------
|
|
154
|
-
//
|
|
155
|
+
// 공개 API
|
|
155
156
|
// -------------------------------------------------------------------
|
|
156
157
|
|
|
157
158
|
return {
|
|
@@ -164,7 +165,7 @@ export function createWebSocketHandler(
|
|
|
164
165
|
try {
|
|
165
166
|
const serviceSocket = createServiceSocket(socket, clientId, clientName, connReq);
|
|
166
167
|
|
|
167
|
-
//
|
|
168
|
+
// 기존 연결 해제
|
|
168
169
|
const prevServiceSocket = socketMap.get(clientId);
|
|
169
170
|
if (prevServiceSocket != null) {
|
|
170
171
|
prevServiceSocket.close();
|
|
@@ -172,32 +173,32 @@ export function createWebSocketHandler(
|
|
|
172
173
|
const connectionDateTimeText =
|
|
173
174
|
prevServiceSocket.connectedAtDateTime.toFormatString("yyyy:MM:dd HH:mm:ss.fff");
|
|
174
175
|
logger.debug(
|
|
175
|
-
|
|
176
|
+
`이전 클라이언트 연결 해제됨: ${clientId}: ${connectionDateTimeText}`,
|
|
176
177
|
);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
socketMap.set(clientId, serviceSocket);
|
|
180
181
|
|
|
181
182
|
serviceSocket.on("close", (code) => {
|
|
182
|
-
logger.debug(
|
|
183
|
+
logger.debug(`클라이언트 연결 해제됨: (code: ${code})`);
|
|
183
184
|
|
|
184
185
|
if (socketMap.get(clientId) !== serviceSocket) return;
|
|
185
186
|
socketMap.delete(clientId);
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
serviceSocket.on("message", async ({ uuid, msg }) => {
|
|
189
|
-
logger.debug("
|
|
190
|
+
logger.debug("요청 수신됨", msg);
|
|
190
191
|
const sentSize = await processRequest(serviceSocket, uuid, msg);
|
|
191
|
-
logger.debug(
|
|
192
|
+
logger.debug(`응답 전송됨 (크기: ${sentSize})`);
|
|
192
193
|
});
|
|
193
194
|
|
|
194
|
-
logger.debug("
|
|
195
|
+
logger.debug("클라이언트 연결됨", {
|
|
195
196
|
clientId,
|
|
196
197
|
remoteAddress: connReq.socket.remoteAddress,
|
|
197
198
|
socketSize: socketMap.size,
|
|
198
199
|
});
|
|
199
200
|
} catch (err) {
|
|
200
|
-
logger.error("
|
|
201
|
+
logger.error("연결 처리 중 에러 발생", err);
|
|
201
202
|
socket.terminate();
|
|
202
203
|
}
|
|
203
204
|
},
|
|
@@ -212,15 +213,14 @@ export function createWebSocketHandler(
|
|
|
212
213
|
clientName: string | undefined,
|
|
213
214
|
changedFileSet: Set<string>,
|
|
214
215
|
): Promise<void> {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
clientName,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
216
|
+
await Promise.allSettled(
|
|
217
|
+
Array.from(socketMap.values()).map((serviceSocket) =>
|
|
218
|
+
serviceSocket.send(Uuid.generate().toString(), {
|
|
219
|
+
name: "reload",
|
|
220
|
+
body: { clientName, changedFileSet },
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
224
|
},
|
|
225
225
|
|
|
226
226
|
async emit<TInfo, TData>(
|
|
@@ -234,18 +234,17 @@ export function createWebSocketHandler(
|
|
|
234
234
|
.filter((item) => infoSelector(item.info as TInfo))
|
|
235
235
|
.map((item) => item.key);
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
keys: subTargetKeys,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
237
|
+
await Promise.allSettled(
|
|
238
|
+
Array.from(socketMap.values()).map(async (subSock) => {
|
|
239
|
+
const subTargetKeys = subSock.filterEventTargetKeys(targetKeys);
|
|
240
|
+
if (subTargetKeys.length > 0) {
|
|
241
|
+
await subSock.send(Uuid.generate().toString(), {
|
|
242
|
+
name: "evt:on",
|
|
243
|
+
body: { keys: subTargetKeys, data },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
249
248
|
},
|
|
250
249
|
};
|
|
251
250
|
}
|