@simplysm/service-common 13.0.0-beta.51 → 13.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.
@@ -0,0 +1,59 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import "@simplysm/core-common";
3
+ import { type ServiceMessage } from "./protocol.types";
4
+ /**
5
+ * 서비스 프로토콜 인터페이스
6
+ *
7
+ * Binary Protocol V2:
8
+ * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
9
+ * - Body: JSON
10
+ * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
11
+ * - 최대 메시지: 100MB
12
+ */
13
+ export interface ServiceProtocol {
14
+ /**
15
+ * 메시지 인코딩 (필요 시 자동 분할)
16
+ */
17
+ encode(uuid: string, message: ServiceMessage): {
18
+ chunks: Bytes[];
19
+ totalSize: number;
20
+ };
21
+ /**
22
+ * 메시지 디코딩 (분할 패킷 자동 조립)
23
+ */
24
+ decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T>;
25
+ /**
26
+ * 프로토콜 인스턴스를 정리한다.
27
+ *
28
+ * 내부 청크 누적기의 GC 타이머를 해제하고 메모리를 정리한다.
29
+ * 프로토콜 인스턴스 사용이 끝나면 반드시 호출해야 한다.
30
+ */
31
+ dispose(): void;
32
+ }
33
+ /**
34
+ * 메시지 디코딩 결과 타입 (유니온)
35
+ *
36
+ * - `type: "complete"`: 모든 청크가 수신되어 메시지 조립이 완료됨
37
+ * - `type: "progress"`: 분할 메시지 수신 중 (일부 청크만 도착)
38
+ */
39
+ export type ServiceMessageDecodeResult<T extends ServiceMessage> = {
40
+ type: "complete";
41
+ uuid: string;
42
+ message: T;
43
+ } | {
44
+ type: "progress";
45
+ uuid: string;
46
+ totalSize: number;
47
+ completedSize: number;
48
+ };
49
+ /**
50
+ * 서비스 프로토콜 인코더/디코더 생성
51
+ *
52
+ * Binary Protocol V2:
53
+ * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
54
+ * - Body: JSON
55
+ * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
56
+ * - 최대 메시지: 100MB
57
+ */
58
+ export declare function createServiceProtocol(): ServiceProtocol;
59
+ //# sourceMappingURL=create-service-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-service-protocol.d.ts","sourceRoot":"","sources":["../../src/protocol/create-service-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,uBAAuB,CAAC;AAE/B,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG;QAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAEtF;;OAEG;IACH,MAAM,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,KAAK,GAAG,0BAA0B,CAAC,CAAC,CAAC,CAAC;IAE9E;;;;;OAKG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,CAAC,CAAC,SAAS,cAAc,IAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAyKvD"}
@@ -0,0 +1,106 @@
1
+ import "@simplysm/core-common";
2
+ import { ArgumentError, jsonStringify, jsonParse, LazyGcMap, Uuid, bytesConcat } from "@simplysm/core-common";
3
+ import { PROTOCOL_CONFIG } from "./protocol.types.js";
4
+ function createServiceProtocol() {
5
+ const accumulator = new LazyGcMap({
6
+ gcInterval: PROTOCOL_CONFIG.GC_INTERVAL,
7
+ expireTime: PROTOCOL_CONFIG.EXPIRE_TIME
8
+ });
9
+ function encodeChunk(header, bodyBytes) {
10
+ const headerBytes = new Uint8Array(28);
11
+ const uuidBytes = new Uuid(header.uuid).toBytes();
12
+ headerBytes.set(uuidBytes, 0);
13
+ const headerView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
14
+ headerView.setBigUint64(16, BigInt(header.totalSize), false);
15
+ headerView.setUint32(24, header.index, false);
16
+ return bytesConcat([headerBytes, ...bodyBytes ? [bodyBytes] : []]);
17
+ }
18
+ return {
19
+ encode(uuid, message) {
20
+ const msgJson = jsonStringify([message.name, ..."body" in message ? [message.body] : []]);
21
+ const msgBytes = new TextEncoder().encode(msgJson);
22
+ const totalSize = msgBytes.length;
23
+ if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
24
+ throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uD06C\uAE30\uAC00 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.", {
25
+ totalSize,
26
+ maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE
27
+ });
28
+ }
29
+ if (totalSize <= PROTOCOL_CONFIG.SPLIT_MESSAGE_SIZE) {
30
+ return { chunks: [encodeChunk({ uuid, totalSize, index: 0 }, msgBytes)], totalSize };
31
+ }
32
+ const chunks = [];
33
+ let offset = 0;
34
+ let index = 0;
35
+ while (offset < totalSize) {
36
+ const chunkBodyBytes = msgBytes.subarray(offset, offset + PROTOCOL_CONFIG.CHUNK_SIZE);
37
+ const chunk = encodeChunk({ uuid, totalSize, index }, chunkBodyBytes);
38
+ chunks.push(chunk);
39
+ offset += PROTOCOL_CONFIG.CHUNK_SIZE;
40
+ index++;
41
+ }
42
+ return { chunks, totalSize };
43
+ },
44
+ decode(bytes) {
45
+ if (bytes.length < 28) {
46
+ throw new ArgumentError("\uBC84\uD37C \uD06C\uAE30\uAC00 \uD5E4\uB354 \uD06C\uAE30\uBCF4\uB2E4 \uC791\uC2B5\uB2C8\uB2E4.", {
47
+ bufferSize: bytes.length,
48
+ minimumSize: 28
49
+ });
50
+ }
51
+ const uuidBytes = bytes.subarray(0, 16);
52
+ const uuid = Uuid.fromBytes(uuidBytes).toString();
53
+ const headerView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
54
+ const totalSize = Number(headerView.getBigUint64(16, false));
55
+ const index = headerView.getUint32(24, false);
56
+ if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
57
+ throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uD06C\uAE30\uAC00 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.", {
58
+ totalSize,
59
+ maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE
60
+ });
61
+ }
62
+ const bodyBytes = bytes.subarray(28);
63
+ const accItem = accumulator.getOrCreate(uuid, () => ({
64
+ totalSize,
65
+ completedSize: 0,
66
+ chunks: []
67
+ }));
68
+ if (accItem.chunks[index] == null) {
69
+ accItem.chunks[index] = bodyBytes;
70
+ accItem.completedSize += bodyBytes.length;
71
+ }
72
+ if (accItem.completedSize < accItem.totalSize) {
73
+ return {
74
+ type: "progress",
75
+ uuid,
76
+ totalSize,
77
+ completedSize: accItem.completedSize
78
+ };
79
+ } else {
80
+ accumulator.delete(uuid);
81
+ const resultBytes = bytesConcat(accItem.chunks.filterExists());
82
+ let messageArr;
83
+ try {
84
+ messageArr = jsonParse(new TextDecoder().decode(resultBytes));
85
+ } catch (err) {
86
+ throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uB514\uCF54\uB529\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.", { uuid, cause: err });
87
+ }
88
+ return {
89
+ type: "complete",
90
+ uuid,
91
+ message: {
92
+ name: messageArr[0],
93
+ body: messageArr[1]
94
+ }
95
+ };
96
+ }
97
+ },
98
+ dispose() {
99
+ accumulator.dispose();
100
+ }
101
+ };
102
+ }
103
+ export {
104
+ createServiceProtocol
105
+ };
106
+ //# sourceMappingURL=create-service-protocol.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/protocol/create-service-protocol.ts"],
4
+ "mappings": "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",
5
+ "names": []
6
+ }
@@ -1,63 +1,3 @@
1
- import type { Bytes } from "@simplysm/core-common";
2
- import "@simplysm/core-common";
3
- import { type ServiceMessage } from "./protocol.types";
4
- /**
5
- * 서비스 프로토콜 인코더/디코더
6
- *
7
- * Binary Protocol V2:
8
- * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
9
- * - Body: JSON
10
- * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
11
- * - 최대 메시지: 100MB
12
- */
13
- export declare class ServiceProtocol {
14
- /**
15
- * 메시지 인코딩 (필요 시 자동 분할)
16
- */
17
- encode(uuid: string, message: ServiceMessage): {
18
- chunks: Bytes[];
19
- totalSize: number;
20
- };
21
- /**
22
- * 메시지 청크 인코딩 (헤더 + 바디)
23
- *
24
- * 헤더 구조 (28 bytes, Big Endian):
25
- * ```
26
- * Offset Size Field
27
- * ------ ---- -----
28
- * 0 16 UUID (binary)
29
- * 16 8 TotalSize (uint64)
30
- * 24 4 Index (uint32)
31
- * ```
32
- */
33
- private _encode;
34
- private readonly _accumulator;
35
- /**
36
- * 프로토콜 인스턴스를 정리한다.
37
- *
38
- * 내부 청크 누적기의 GC 타이머를 해제하고 메모리를 정리한다.
39
- * 프로토콜 인스턴스 사용이 끝나면 반드시 호출해야 한다.
40
- */
41
- dispose(): void;
42
- /**
43
- * 메시지 디코딩 (분할 패킷 자동 조립)
44
- */
45
- decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T>;
46
- }
47
- /**
48
- * 메시지 디코딩 결과 타입 (유니온)
49
- *
50
- * - `type: "complete"`: 모든 청크가 수신되어 메시지 조립이 완료됨
51
- * - `type: "progress"`: 분할 메시지 수신 중 (일부 청크만 도착)
52
- */
53
- export type ServiceMessageDecodeResult<T extends ServiceMessage> = {
54
- type: "complete";
55
- uuid: string;
56
- message: T;
57
- } | {
58
- type: "progress";
59
- uuid: string;
60
- totalSize: number;
61
- completedSize: number;
62
- };
1
+ export type { ServiceProtocol, ServiceMessageDecodeResult } from "./create-service-protocol";
2
+ export { createServiceProtocol } from "./create-service-protocol";
63
3
  //# sourceMappingURL=service-protocol.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service-protocol.d.ts","sourceRoot":"","sources":["../../src/protocol/service-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,uBAAuB,CAAC;AAE/B,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAK1B;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG;QAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAqCrF;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,OAAO;IA0Bf,OAAO,CAAC,QAAQ,CAAC,YAAY,CAU1B;IAEH;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAIf;;OAEG;IACH,MAAM,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,KAAK,GAAG,0BAA0B,CAAC,CAAC,CAAC;CAmE9E;AAED;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,CAAC,CAAC,SAAS,cAAc,IAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC"}
1
+ {"version":3,"file":"service-protocol.d.ts","sourceRoot":"","sources":["../../src/protocol/service-protocol.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAC7F,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -1,134 +1,5 @@
1
- import "@simplysm/core-common";
2
- import { ArgumentError, jsonStringify, jsonParse, LazyGcMap, Uuid, bytesConcat } from "@simplysm/core-common";
3
- import { PROTOCOL_CONFIG } from "./protocol.types.js";
4
- class ServiceProtocol {
5
- // -------------------------------------------------------------------
6
- // Encoding
7
- // -------------------------------------------------------------------
8
- /**
9
- * 메시지 인코딩 (필요 시 자동 분할)
10
- */
11
- encode(uuid, message) {
12
- const msgJson = jsonStringify([message.name, ..."body" in message ? [message.body] : []]);
13
- const msgBytes = new TextEncoder().encode(msgJson);
14
- const totalSize = msgBytes.length;
15
- if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
16
- throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uD06C\uAE30\uAC00 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.", {
17
- totalSize,
18
- maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE
19
- });
20
- }
21
- if (totalSize <= PROTOCOL_CONFIG.SPLIT_MESSAGE_SIZE) {
22
- return { chunks: [this._encode({ uuid, totalSize, index: 0 }, msgBytes)], totalSize };
23
- }
24
- const chunks = [];
25
- let offset = 0;
26
- let index = 0;
27
- while (offset < totalSize) {
28
- const chunkBodyBytes = msgBytes.subarray(offset, offset + PROTOCOL_CONFIG.CHUNK_SIZE);
29
- const chunk = this._encode({ uuid, totalSize, index }, chunkBodyBytes);
30
- chunks.push(chunk);
31
- offset += PROTOCOL_CONFIG.CHUNK_SIZE;
32
- index++;
33
- }
34
- return { chunks, totalSize };
35
- }
36
- /**
37
- * 메시지 청크 인코딩 (헤더 + 바디)
38
- *
39
- * 헤더 구조 (28 bytes, Big Endian):
40
- * ```
41
- * Offset Size Field
42
- * ------ ---- -----
43
- * 0 16 UUID (binary)
44
- * 16 8 TotalSize (uint64)
45
- * 24 4 Index (uint32)
46
- * ```
47
- */
48
- _encode(header, bodyBytes) {
49
- const headerBytes = new Uint8Array(28);
50
- const uuidBytes = new Uuid(header.uuid).toBytes();
51
- headerBytes.set(uuidBytes, 0);
52
- const headerView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
53
- headerView.setBigUint64(16, BigInt(header.totalSize), false);
54
- headerView.setUint32(24, header.index, false);
55
- return bytesConcat([headerBytes, ...bodyBytes ? [bodyBytes] : []]);
56
- }
57
- // -------------------------------------------------------------------
58
- // Decoding
59
- // -------------------------------------------------------------------
60
- _accumulator = new LazyGcMap({
61
- gcInterval: PROTOCOL_CONFIG.GC_INTERVAL,
62
- expireTime: PROTOCOL_CONFIG.EXPIRE_TIME
63
- });
64
- /**
65
- * 프로토콜 인스턴스를 정리한다.
66
- *
67
- * 내부 청크 누적기의 GC 타이머를 해제하고 메모리를 정리한다.
68
- * 프로토콜 인스턴스 사용이 끝나면 반드시 호출해야 한다.
69
- */
70
- dispose() {
71
- this._accumulator.dispose();
72
- }
73
- /**
74
- * 메시지 디코딩 (분할 패킷 자동 조립)
75
- */
76
- decode(bytes) {
77
- if (bytes.length < 28) {
78
- throw new ArgumentError("\uBC84\uD37C \uD06C\uAE30\uAC00 \uD5E4\uB354 \uD06C\uAE30\uBCF4\uB2E4 \uC791\uC2B5\uB2C8\uB2E4.", {
79
- bufferSize: bytes.length,
80
- minimumSize: 28
81
- });
82
- }
83
- const uuidBytes = bytes.subarray(0, 16);
84
- const uuid = Uuid.fromBytes(uuidBytes).toString();
85
- const headerView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
86
- const totalSize = Number(headerView.getBigUint64(16, false));
87
- const index = headerView.getUint32(24, false);
88
- if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
89
- throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uD06C\uAE30\uAC00 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.", {
90
- totalSize,
91
- maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE
92
- });
93
- }
94
- const bodyBytes = bytes.subarray(28);
95
- const accItem = this._accumulator.getOrCreate(uuid, () => ({
96
- totalSize,
97
- completedSize: 0,
98
- chunks: []
99
- }));
100
- if (accItem.chunks[index] == null) {
101
- accItem.chunks[index] = bodyBytes;
102
- accItem.completedSize += bodyBytes.length;
103
- }
104
- if (accItem.completedSize < accItem.totalSize) {
105
- return {
106
- type: "progress",
107
- uuid,
108
- totalSize,
109
- completedSize: accItem.completedSize
110
- };
111
- } else {
112
- this._accumulator.delete(uuid);
113
- const resultBytes = bytesConcat(accItem.chunks.filterExists());
114
- let messageArr;
115
- try {
116
- messageArr = jsonParse(new TextDecoder().decode(resultBytes));
117
- } catch (err) {
118
- throw new ArgumentError("\uBA54\uC2DC\uC9C0 \uB514\uCF54\uB529\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.", { uuid, cause: err });
119
- }
120
- return {
121
- type: "complete",
122
- uuid,
123
- message: {
124
- name: messageArr[0],
125
- body: messageArr[1]
126
- }
127
- };
128
- }
129
- }
130
- }
1
+ import { createServiceProtocol } from "./create-service-protocol.js";
131
2
  export {
132
- ServiceProtocol
3
+ createServiceProtocol
133
4
  };
134
5
  //# sourceMappingURL=service-protocol.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/protocol/service-protocol.ts"],
4
- "mappings": "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",
4
+ "mappings": "AACA,SAAS,6BAA6B;",
5
5
  "names": []
6
6
  }
package/dist/types.d.ts CHANGED
@@ -1,32 +1,3 @@
1
- /**
2
- * 이벤트 리스너 타입 정의용 추상 클래스
3
- *
4
- * - 상속만 하면 됨 (프로퍼티 구현 불필요)
5
- * - $info, $data는 타입 추출용 (런타임 미사용)
6
- * - eventName은 mangle 안전한 이벤트 식별자
7
- *
8
- * @example
9
- * export class SharedDataChangeEvent extends ServiceEventListener<
10
- * { name: string; filter: unknown },
11
- * (string | number)[] | undefined
12
- * > {
13
- * readonly eventName = "SharedDataChangeEvent";
14
- * }
15
- *
16
- * // 클라이언트에서 사용
17
- * await client.addEventListener(
18
- * SharedDataChangeEvent,
19
- * { name: "test", filter: null },
20
- * (data) => console.log(data)
21
- * );
22
- */
23
- export declare abstract class ServiceEventListener<TInfo, TData> {
24
- /** mangle 안전한 이벤트 식별자 (상속 시 필수 구현) */
25
- abstract readonly eventName: string;
26
- /** 타입 추출용 (런타임 미사용) */
27
- readonly $info: TInfo;
28
- readonly $data: TData;
29
- }
30
1
  /**
31
2
  * 파일 업로드 결과
32
3
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8BAAsB,oBAAoB,CAAC,KAAK,EAAE,KAAK;IACrD,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAEpC,uBAAuB;IACvB,SAAiB,KAAK,EAAE,KAAK,CAAC;IAC9B,SAAiB,KAAK,EAAE,KAAK,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;CACd"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;CACd"}
package/dist/types.js CHANGED
@@ -1,6 +1 @@
1
- class ServiceEventListener {
2
- }
3
- export {
4
- ServiceEventListener
5
- };
6
1
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/types.ts"],
4
- "mappings": "AAsBO,MAAe,qBAAmC;AAOzD;",
3
+ "sources": [],
4
+ "mappings": "",
5
5
  "names": []
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simplysm/service-common",
3
3
  "sideEffects": false,
4
- "version": "13.0.0-beta.51",
4
+ "version": "13.0.1",
5
5
  "description": "심플리즘 패키지 - 서비스 모듈 (common)",
6
6
  "author": "김석래",
7
7
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "src"
19
19
  ],
20
20
  "dependencies": {
21
- "@simplysm/core-common": "13.0.0-beta.51",
22
- "@simplysm/orm-common": "13.0.0-beta.51"
21
+ "@simplysm/core-common": "13.0.1",
22
+ "@simplysm/orm-common": "13.0.1"
23
23
  }
24
24
  }
@@ -0,0 +1,223 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import "@simplysm/core-common";
3
+ import { ArgumentError, jsonStringify, jsonParse, LazyGcMap, Uuid, bytesConcat } from "@simplysm/core-common";
4
+ import { PROTOCOL_CONFIG, type ServiceMessage } from "./protocol.types";
5
+
6
+ /**
7
+ * 서비스 프로토콜 인터페이스
8
+ *
9
+ * Binary Protocol V2:
10
+ * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
11
+ * - Body: JSON
12
+ * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
13
+ * - 최대 메시지: 100MB
14
+ */
15
+ export interface ServiceProtocol {
16
+ /**
17
+ * 메시지 인코딩 (필요 시 자동 분할)
18
+ */
19
+ encode(uuid: string, message: ServiceMessage): { chunks: Bytes[]; totalSize: number };
20
+
21
+ /**
22
+ * 메시지 디코딩 (분할 패킷 자동 조립)
23
+ */
24
+ decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T>;
25
+
26
+ /**
27
+ * 프로토콜 인스턴스를 정리한다.
28
+ *
29
+ * 내부 청크 누적기의 GC 타이머를 해제하고 메모리를 정리한다.
30
+ * 프로토콜 인스턴스 사용이 끝나면 반드시 호출해야 한다.
31
+ */
32
+ dispose(): void;
33
+ }
34
+
35
+ /**
36
+ * 메시지 디코딩 결과 타입 (유니온)
37
+ *
38
+ * - `type: "complete"`: 모든 청크가 수신되어 메시지 조립이 완료됨
39
+ * - `type: "progress"`: 분할 메시지 수신 중 (일부 청크만 도착)
40
+ */
41
+ export type ServiceMessageDecodeResult<T extends ServiceMessage> =
42
+ | { type: "complete"; uuid: string; message: T }
43
+ | { type: "progress"; uuid: string; totalSize: number; completedSize: number };
44
+
45
+ /**
46
+ * 서비스 프로토콜 인코더/디코더 생성
47
+ *
48
+ * Binary Protocol V2:
49
+ * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
50
+ * - Body: JSON
51
+ * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
52
+ * - 최대 메시지: 100MB
53
+ */
54
+ export function createServiceProtocol(): ServiceProtocol {
55
+ // -------------------------------------------------------------------
56
+ // State
57
+ // -------------------------------------------------------------------
58
+
59
+ const accumulator = new LazyGcMap<
60
+ string,
61
+ {
62
+ totalSize: number;
63
+ completedSize: number;
64
+ chunks: (Bytes | undefined)[];
65
+ }
66
+ >({
67
+ gcInterval: PROTOCOL_CONFIG.GC_INTERVAL,
68
+ expireTime: PROTOCOL_CONFIG.EXPIRE_TIME,
69
+ });
70
+
71
+ // -------------------------------------------------------------------
72
+ // Encoding Helper
73
+ // -------------------------------------------------------------------
74
+
75
+ /**
76
+ * 메시지 청크 인코딩 (헤더 + 바디)
77
+ *
78
+ * 헤더 구조 (28 bytes, Big Endian):
79
+ * ```
80
+ * Offset Size Field
81
+ * ------ ---- -----
82
+ * 0 16 UUID (binary)
83
+ * 16 8 TotalSize (uint64)
84
+ * 24 4 Index (uint32)
85
+ * ```
86
+ */
87
+ function encodeChunk(
88
+ header: {
89
+ uuid: string;
90
+ totalSize: number;
91
+ index: number;
92
+ },
93
+ bodyBytes?: Bytes,
94
+ ): Bytes {
95
+ const headerBytes = new Uint8Array(28);
96
+
97
+ // UUID (0-15)
98
+ const uuidBytes = new Uuid(header.uuid).toBytes();
99
+ headerBytes.set(uuidBytes, 0);
100
+
101
+ // TotalSize (16-23), Index (24-27)
102
+ const headerView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
103
+ headerView.setBigUint64(16, BigInt(header.totalSize), false);
104
+ headerView.setUint32(24, header.index, false);
105
+
106
+ return bytesConcat([headerBytes, ...(bodyBytes ? [bodyBytes] : [])]);
107
+ }
108
+
109
+ // -------------------------------------------------------------------
110
+ // Public API
111
+ // -------------------------------------------------------------------
112
+
113
+ return {
114
+ encode(uuid: string, message: ServiceMessage): { chunks: Bytes[]; totalSize: number } {
115
+ const msgJson = jsonStringify([message.name, ...("body" in message ? [message.body] : [])]);
116
+ const msgBytes = new TextEncoder().encode(msgJson);
117
+
118
+ const totalSize = msgBytes.length;
119
+
120
+ // 전체 사이즈 제한 체크 (가장 먼저 수행)
121
+ if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
122
+ throw new ArgumentError("메시지 크기가 제한을 초과했습니다.", {
123
+ totalSize,
124
+ maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE,
125
+ });
126
+ }
127
+
128
+ // 사이즈가 작으면 그대로 반환
129
+ if (totalSize <= PROTOCOL_CONFIG.SPLIT_MESSAGE_SIZE) {
130
+ return { chunks: [encodeChunk({ uuid, totalSize, index: 0 }, msgBytes)], totalSize };
131
+ }
132
+
133
+ // 분할 처리
134
+ const chunks: Bytes[] = [];
135
+ let offset = 0;
136
+ let index = 0;
137
+
138
+ while (offset < totalSize) {
139
+ const chunkBodyBytes = msgBytes.subarray(offset, offset + PROTOCOL_CONFIG.CHUNK_SIZE);
140
+
141
+ const chunk = encodeChunk({ uuid, totalSize, index }, chunkBodyBytes);
142
+ chunks.push(chunk);
143
+
144
+ offset += PROTOCOL_CONFIG.CHUNK_SIZE;
145
+ index++;
146
+ }
147
+
148
+ return { chunks, totalSize };
149
+ },
150
+
151
+ decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T> {
152
+ if (bytes.length < 28) {
153
+ throw new ArgumentError("버퍼 크기가 헤더 크기보다 작습니다.", {
154
+ bufferSize: bytes.length,
155
+ minimumSize: 28,
156
+ });
157
+ }
158
+
159
+ // 1. 헤더 읽기
160
+
161
+ // UUID
162
+ const uuidBytes = bytes.subarray(0, 16);
163
+ const uuid = Uuid.fromBytes(uuidBytes).toString();
164
+
165
+ // TOTAL_SIZE, INDEX
166
+ const headerView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
167
+ const totalSize = Number(headerView.getBigUint64(16, false));
168
+ const index = headerView.getUint32(24, false);
169
+
170
+ // 전체 사이즈 제한 체크 (가장 먼저 수행)
171
+ if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
172
+ throw new ArgumentError("메시지 크기가 제한을 초과했습니다.", {
173
+ totalSize,
174
+ maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE,
175
+ });
176
+ }
177
+
178
+ const bodyBytes = bytes.subarray(28);
179
+
180
+ const accItem = accumulator.getOrCreate(uuid, () => ({
181
+ totalSize,
182
+ completedSize: 0,
183
+ chunks: [],
184
+ }));
185
+ if (accItem.chunks[index] == null) {
186
+ // 패킷중복 방어
187
+ accItem.chunks[index] = bodyBytes;
188
+ accItem.completedSize += bodyBytes.length;
189
+ }
190
+
191
+ if (accItem.completedSize < accItem.totalSize) {
192
+ return {
193
+ type: "progress",
194
+ uuid: uuid,
195
+ totalSize: totalSize,
196
+ completedSize: accItem.completedSize,
197
+ };
198
+ } else {
199
+ accumulator.delete(uuid); // 메모리 해제
200
+
201
+ const resultBytes = bytesConcat(accItem.chunks.filterExists());
202
+ let messageArr: [string, unknown];
203
+ try {
204
+ messageArr = jsonParse<[string, unknown]>(new TextDecoder().decode(resultBytes));
205
+ } catch (err) {
206
+ throw new ArgumentError("메시지 디코딩에 실패했습니다.", { uuid, cause: err });
207
+ }
208
+ return {
209
+ type: "complete",
210
+ uuid: uuid,
211
+ message: {
212
+ name: messageArr[0],
213
+ body: messageArr[1],
214
+ } as T,
215
+ };
216
+ }
217
+ },
218
+
219
+ dispose(): void {
220
+ accumulator.dispose();
221
+ },
222
+ };
223
+ }
@@ -1,200 +1,2 @@
1
- import type { Bytes } from "@simplysm/core-common";
2
- import "@simplysm/core-common";
3
- import { ArgumentError, jsonStringify, jsonParse, LazyGcMap, Uuid, bytesConcat } from "@simplysm/core-common";
4
- import { PROTOCOL_CONFIG, type ServiceMessage } from "./protocol.types";
5
-
6
- /**
7
- * 서비스 프로토콜 인코더/디코더
8
- *
9
- * Binary Protocol V2:
10
- * - Header: 28 bytes (UUID 16 + TotalSize 8 + Index 4)
11
- * - Body: JSON
12
- * - 자동 청킹: 3MB 초과 시 300KB 단위 분할
13
- * - 최대 메시지: 100MB
14
- */
15
- export class ServiceProtocol {
16
- // -------------------------------------------------------------------
17
- // Encoding
18
- // -------------------------------------------------------------------
19
-
20
- /**
21
- * 메시지 인코딩 (필요 시 자동 분할)
22
- */
23
- encode(uuid: string, message: ServiceMessage): { chunks: Bytes[]; totalSize: number } {
24
- const msgJson = jsonStringify([message.name, ...("body" in message ? [message.body] : [])]);
25
- const msgBytes = new TextEncoder().encode(msgJson);
26
-
27
- const totalSize = msgBytes.length;
28
-
29
- // 전체 사이즈 제한 체크 (가장 먼저 수행)
30
- if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
31
- throw new ArgumentError("메시지 크기가 제한을 초과했습니다.", {
32
- totalSize,
33
- maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE,
34
- });
35
- }
36
-
37
- // 사이즈가 작으면 그대로 반환
38
- if (totalSize <= PROTOCOL_CONFIG.SPLIT_MESSAGE_SIZE) {
39
- return { chunks: [this._encode({ uuid, totalSize, index: 0 }, msgBytes)], totalSize };
40
- }
41
-
42
- // 분할 처리
43
- const chunks: Bytes[] = [];
44
- let offset = 0;
45
- let index = 0;
46
-
47
- while (offset < totalSize) {
48
- const chunkBodyBytes = msgBytes.subarray(offset, offset + PROTOCOL_CONFIG.CHUNK_SIZE);
49
-
50
- const chunk = this._encode({ uuid, totalSize, index }, chunkBodyBytes);
51
- chunks.push(chunk);
52
-
53
- offset += PROTOCOL_CONFIG.CHUNK_SIZE;
54
- index++;
55
- }
56
-
57
- return { chunks, totalSize };
58
- }
59
-
60
- /**
61
- * 메시지 청크 인코딩 (헤더 + 바디)
62
- *
63
- * 헤더 구조 (28 bytes, Big Endian):
64
- * ```
65
- * Offset Size Field
66
- * ------ ---- -----
67
- * 0 16 UUID (binary)
68
- * 16 8 TotalSize (uint64)
69
- * 24 4 Index (uint32)
70
- * ```
71
- */
72
- private _encode(
73
- header: {
74
- uuid: string;
75
- totalSize: number;
76
- index: number;
77
- },
78
- bodyBytes?: Bytes,
79
- ): Bytes {
80
- const headerBytes = new Uint8Array(28);
81
-
82
- // UUID (0-15)
83
- const uuidBytes = new Uuid(header.uuid).toBytes();
84
- headerBytes.set(uuidBytes, 0);
85
-
86
- // TotalSize (16-23), Index (24-27)
87
- const headerView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
88
- headerView.setBigUint64(16, BigInt(header.totalSize), false);
89
- headerView.setUint32(24, header.index, false);
90
-
91
- return bytesConcat([headerBytes, ...(bodyBytes ? [bodyBytes] : [])]);
92
- }
93
-
94
- // -------------------------------------------------------------------
95
- // Decoding
96
- // -------------------------------------------------------------------
97
-
98
- private readonly _accumulator = new LazyGcMap<
99
- string,
100
- {
101
- totalSize: number;
102
- completedSize: number;
103
- chunks: (Bytes | undefined)[];
104
- }
105
- >({
106
- gcInterval: PROTOCOL_CONFIG.GC_INTERVAL,
107
- expireTime: PROTOCOL_CONFIG.EXPIRE_TIME,
108
- });
109
-
110
- /**
111
- * 프로토콜 인스턴스를 정리한다.
112
- *
113
- * 내부 청크 누적기의 GC 타이머를 해제하고 메모리를 정리한다.
114
- * 프로토콜 인스턴스 사용이 끝나면 반드시 호출해야 한다.
115
- */
116
- dispose(): void {
117
- this._accumulator.dispose();
118
- }
119
-
120
- /**
121
- * 메시지 디코딩 (분할 패킷 자동 조립)
122
- */
123
- decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T> {
124
- if (bytes.length < 28) {
125
- throw new ArgumentError("버퍼 크기가 헤더 크기보다 작습니다.", {
126
- bufferSize: bytes.length,
127
- minimumSize: 28,
128
- });
129
- }
130
-
131
- // 1. 헤더 읽기
132
-
133
- // UUID
134
- const uuidBytes = bytes.subarray(0, 16);
135
- const uuid = Uuid.fromBytes(uuidBytes).toString();
136
-
137
- // TOTAL_SIZE, INDEX
138
- const headerView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
139
- const totalSize = Number(headerView.getBigUint64(16, false));
140
- const index = headerView.getUint32(24, false);
141
-
142
- // 전체 사이즈 제한 체크 (가장 먼저 수행)
143
- if (totalSize > PROTOCOL_CONFIG.MAX_TOTAL_SIZE) {
144
- throw new ArgumentError("메시지 크기가 제한을 초과했습니다.", {
145
- totalSize,
146
- maxSize: PROTOCOL_CONFIG.MAX_TOTAL_SIZE,
147
- });
148
- }
149
-
150
- const bodyBytes = bytes.subarray(28);
151
-
152
- const accItem = this._accumulator.getOrCreate(uuid, () => ({
153
- totalSize,
154
- completedSize: 0,
155
- chunks: [],
156
- }));
157
- if (accItem.chunks[index] == null) {
158
- // 패킷중복 방어
159
- accItem.chunks[index] = bodyBytes;
160
- accItem.completedSize += bodyBytes.length;
161
- }
162
-
163
- if (accItem.completedSize < accItem.totalSize) {
164
- return {
165
- type: "progress",
166
- uuid: uuid,
167
- totalSize: totalSize,
168
- completedSize: accItem.completedSize,
169
- };
170
- } else {
171
- this._accumulator.delete(uuid); // 메모리 해제
172
-
173
- const resultBytes = bytesConcat(accItem.chunks.filterExists());
174
- let messageArr: [string, unknown];
175
- try {
176
- messageArr = jsonParse<[string, unknown]>(new TextDecoder().decode(resultBytes));
177
- } catch (err) {
178
- throw new ArgumentError("메시지 디코딩에 실패했습니다.", { uuid, cause: err });
179
- }
180
- return {
181
- type: "complete",
182
- uuid: uuid,
183
- message: {
184
- name: messageArr[0],
185
- body: messageArr[1],
186
- } as T,
187
- };
188
- }
189
- }
190
- }
191
-
192
- /**
193
- * 메시지 디코딩 결과 타입 (유니온)
194
- *
195
- * - `type: "complete"`: 모든 청크가 수신되어 메시지 조립이 완료됨
196
- * - `type: "progress"`: 분할 메시지 수신 중 (일부 청크만 도착)
197
- */
198
- export type ServiceMessageDecodeResult<T extends ServiceMessage> =
199
- | { type: "complete"; uuid: string; message: T }
200
- | { type: "progress"; uuid: string; totalSize: number; completedSize: number };
1
+ export type { ServiceProtocol, ServiceMessageDecodeResult } from "./create-service-protocol";
2
+ export { createServiceProtocol } from "./create-service-protocol";
package/src/types.ts CHANGED
@@ -1,34 +1,3 @@
1
- /**
2
- * 이벤트 리스너 타입 정의용 추상 클래스
3
- *
4
- * - 상속만 하면 됨 (프로퍼티 구현 불필요)
5
- * - $info, $data는 타입 추출용 (런타임 미사용)
6
- * - eventName은 mangle 안전한 이벤트 식별자
7
- *
8
- * @example
9
- * export class SharedDataChangeEvent extends ServiceEventListener<
10
- * { name: string; filter: unknown },
11
- * (string | number)[] | undefined
12
- * > {
13
- * readonly eventName = "SharedDataChangeEvent";
14
- * }
15
- *
16
- * // 클라이언트에서 사용
17
- * await client.addEventListener(
18
- * SharedDataChangeEvent,
19
- * { name: "test", filter: null },
20
- * (data) => console.log(data)
21
- * );
22
- */
23
- export abstract class ServiceEventListener<TInfo, TData> {
24
- /** mangle 안전한 이벤트 식별자 (상속 시 필수 구현) */
25
- abstract readonly eventName: string;
26
-
27
- /** 타입 추출용 (런타임 미사용) */
28
- declare readonly $info: TInfo;
29
- declare readonly $data: TData;
30
- }
31
-
32
1
  /**
33
2
  * 파일 업로드 결과
34
3
  *