@simplysm/service-client 13.0.0-beta.6

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.
Files changed (209) hide show
  1. package/.cache/typecheck-browser.tsbuildinfo +1 -0
  2. package/.cache/typecheck-node.tsbuildinfo +1 -0
  3. package/README.md +397 -0
  4. package/dist/core-common/src/common.types.d.ts +74 -0
  5. package/dist/core-common/src/common.types.d.ts.map +1 -0
  6. package/dist/core-common/src/env.d.ts +6 -0
  7. package/dist/core-common/src/env.d.ts.map +1 -0
  8. package/dist/core-common/src/errors/argument-error.d.ts +25 -0
  9. package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
  10. package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
  11. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
  12. package/dist/core-common/src/errors/sd-error.d.ts +27 -0
  13. package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
  14. package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
  15. package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
  16. package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
  17. package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
  18. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
  19. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
  20. package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
  21. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
  22. package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
  23. package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
  24. package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
  25. package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
  26. package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
  27. package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
  28. package/dist/core-common/src/features/event-emitter.d.ts +66 -0
  29. package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
  30. package/dist/core-common/src/features/serial-queue.d.ts +47 -0
  31. package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
  32. package/dist/core-common/src/index.d.ts +32 -0
  33. package/dist/core-common/src/index.d.ts.map +1 -0
  34. package/dist/core-common/src/types/date-only.d.ts +152 -0
  35. package/dist/core-common/src/types/date-only.d.ts.map +1 -0
  36. package/dist/core-common/src/types/date-time.d.ts +96 -0
  37. package/dist/core-common/src/types/date-time.d.ts.map +1 -0
  38. package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
  39. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
  40. package/dist/core-common/src/types/time.d.ts +68 -0
  41. package/dist/core-common/src/types/time.d.ts.map +1 -0
  42. package/dist/core-common/src/types/uuid.d.ts +35 -0
  43. package/dist/core-common/src/types/uuid.d.ts.map +1 -0
  44. package/dist/core-common/src/utils/bytes.d.ts +51 -0
  45. package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
  46. package/dist/core-common/src/utils/date-format.d.ts +90 -0
  47. package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
  48. package/dist/core-common/src/utils/json.d.ts +34 -0
  49. package/dist/core-common/src/utils/json.d.ts.map +1 -0
  50. package/dist/core-common/src/utils/num.d.ts +60 -0
  51. package/dist/core-common/src/utils/num.d.ts.map +1 -0
  52. package/dist/core-common/src/utils/obj.d.ts +258 -0
  53. package/dist/core-common/src/utils/obj.d.ts.map +1 -0
  54. package/dist/core-common/src/utils/path.d.ts +23 -0
  55. package/dist/core-common/src/utils/path.d.ts.map +1 -0
  56. package/dist/core-common/src/utils/primitive.d.ts +18 -0
  57. package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
  58. package/dist/core-common/src/utils/str.d.ts +103 -0
  59. package/dist/core-common/src/utils/str.d.ts.map +1 -0
  60. package/dist/core-common/src/utils/template-strings.d.ts +84 -0
  61. package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
  62. package/dist/core-common/src/utils/transferable.d.ts +47 -0
  63. package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
  64. package/dist/core-common/src/utils/wait.d.ts +19 -0
  65. package/dist/core-common/src/utils/wait.d.ts.map +1 -0
  66. package/dist/core-common/src/utils/xml.d.ts +36 -0
  67. package/dist/core-common/src/utils/xml.d.ts.map +1 -0
  68. package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
  69. package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
  70. package/dist/features/event-client.js +74 -0
  71. package/dist/features/event-client.js.map +7 -0
  72. package/dist/features/file-client.js +42 -0
  73. package/dist/features/file-client.js.map +7 -0
  74. package/dist/features/orm/orm-client-connector.js +41 -0
  75. package/dist/features/orm/orm-client-connector.js.map +7 -0
  76. package/dist/features/orm/orm-client-db-context-executor.js +61 -0
  77. package/dist/features/orm/orm-client-db-context-executor.js.map +7 -0
  78. package/dist/features/orm/orm-connect-config.js +1 -0
  79. package/dist/features/orm/orm-connect-config.js.map +7 -0
  80. package/dist/index.js +12 -0
  81. package/dist/index.js.map +7 -0
  82. package/dist/orm-common/src/db-context.d.ts +669 -0
  83. package/dist/orm-common/src/db-context.d.ts.map +1 -0
  84. package/dist/orm-common/src/errors/db-transaction-error.d.ts +51 -0
  85. package/dist/orm-common/src/errors/db-transaction-error.d.ts.map +1 -0
  86. package/dist/orm-common/src/exec/executable.d.ts +79 -0
  87. package/dist/orm-common/src/exec/executable.d.ts.map +1 -0
  88. package/dist/orm-common/src/exec/queryable.d.ts +708 -0
  89. package/dist/orm-common/src/exec/queryable.d.ts.map +1 -0
  90. package/dist/orm-common/src/exec/search-parser.d.ts +72 -0
  91. package/dist/orm-common/src/exec/search-parser.d.ts.map +1 -0
  92. package/dist/orm-common/src/expr/expr-unit.d.ts +25 -0
  93. package/dist/orm-common/src/expr/expr-unit.d.ts.map +1 -0
  94. package/dist/orm-common/src/expr/expr.d.ts +1369 -0
  95. package/dist/orm-common/src/expr/expr.d.ts.map +1 -0
  96. package/dist/orm-common/src/index.d.ts +32 -0
  97. package/dist/orm-common/src/index.d.ts.map +1 -0
  98. package/dist/orm-common/src/models/system-migration.d.ts +10 -0
  99. package/dist/orm-common/src/models/system-migration.d.ts.map +1 -0
  100. package/dist/orm-common/src/query-builder/base/expr-renderer-base.d.ts +95 -0
  101. package/dist/orm-common/src/query-builder/base/expr-renderer-base.d.ts.map +1 -0
  102. package/dist/orm-common/src/query-builder/base/query-builder-base.d.ts +66 -0
  103. package/dist/orm-common/src/query-builder/base/query-builder-base.d.ts.map +1 -0
  104. package/dist/orm-common/src/query-builder/mssql/mssql-expr-renderer.d.ts +84 -0
  105. package/dist/orm-common/src/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -0
  106. package/dist/orm-common/src/query-builder/mssql/mssql-query-builder.d.ts +45 -0
  107. package/dist/orm-common/src/query-builder/mssql/mssql-query-builder.d.ts.map +1 -0
  108. package/dist/orm-common/src/query-builder/mysql/mysql-expr-renderer.d.ts +84 -0
  109. package/dist/orm-common/src/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -0
  110. package/dist/orm-common/src/query-builder/mysql/mysql-query-builder.d.ts +54 -0
  111. package/dist/orm-common/src/query-builder/mysql/mysql-query-builder.d.ts.map +1 -0
  112. package/dist/orm-common/src/query-builder/postgresql/postgresql-expr-renderer.d.ts +84 -0
  113. package/dist/orm-common/src/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -0
  114. package/dist/orm-common/src/query-builder/postgresql/postgresql-query-builder.d.ts +52 -0
  115. package/dist/orm-common/src/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -0
  116. package/dist/orm-common/src/query-builder/query-builder.d.ts +7 -0
  117. package/dist/orm-common/src/query-builder/query-builder.d.ts.map +1 -0
  118. package/dist/orm-common/src/schema/factory/column-builder.d.ts +394 -0
  119. package/dist/orm-common/src/schema/factory/column-builder.d.ts.map +1 -0
  120. package/dist/orm-common/src/schema/factory/index-builder.d.ts +151 -0
  121. package/dist/orm-common/src/schema/factory/index-builder.d.ts.map +1 -0
  122. package/dist/orm-common/src/schema/factory/relation-builder.d.ts +337 -0
  123. package/dist/orm-common/src/schema/factory/relation-builder.d.ts.map +1 -0
  124. package/dist/orm-common/src/schema/procedure-builder.d.ts +202 -0
  125. package/dist/orm-common/src/schema/procedure-builder.d.ts.map +1 -0
  126. package/dist/orm-common/src/schema/table-builder.d.ts +259 -0
  127. package/dist/orm-common/src/schema/table-builder.d.ts.map +1 -0
  128. package/dist/orm-common/src/schema/view-builder.d.ts +183 -0
  129. package/dist/orm-common/src/schema/view-builder.d.ts.map +1 -0
  130. package/dist/orm-common/src/types/column.d.ts +172 -0
  131. package/dist/orm-common/src/types/column.d.ts.map +1 -0
  132. package/dist/orm-common/src/types/db.d.ts +175 -0
  133. package/dist/orm-common/src/types/db.d.ts.map +1 -0
  134. package/dist/orm-common/src/types/expr.d.ts +474 -0
  135. package/dist/orm-common/src/types/expr.d.ts.map +1 -0
  136. package/dist/orm-common/src/types/query-def.d.ts +351 -0
  137. package/dist/orm-common/src/types/query-def.d.ts.map +1 -0
  138. package/dist/orm-common/src/utils/result-parser.d.ts +38 -0
  139. package/dist/orm-common/src/utils/result-parser.d.ts.map +1 -0
  140. package/dist/protocol/client-protocol-wrapper.js +92 -0
  141. package/dist/protocol/client-protocol-wrapper.js.map +7 -0
  142. package/dist/service-client/src/features/event-client.d.ts +14 -0
  143. package/dist/service-client/src/features/event-client.d.ts.map +1 -0
  144. package/dist/service-client/src/features/file-client.d.ts +13 -0
  145. package/dist/service-client/src/features/file-client.d.ts.map +1 -0
  146. package/dist/service-client/src/features/orm/orm-client-connector.d.ts +10 -0
  147. package/dist/service-client/src/features/orm/orm-client-connector.d.ts.map +1 -0
  148. package/dist/service-client/src/features/orm/orm-client-db-context-executor.d.ts +26 -0
  149. package/dist/service-client/src/features/orm/orm-client-db-context-executor.d.ts.map +1 -0
  150. package/dist/service-client/src/features/orm/orm-connect-config.d.ts +13 -0
  151. package/dist/service-client/src/features/orm/orm-connect-config.d.ts.map +1 -0
  152. package/dist/service-client/src/index.d.ts +12 -0
  153. package/dist/service-client/src/index.d.ts.map +1 -0
  154. package/dist/service-client/src/protocol/client-protocol-wrapper.d.ts +23 -0
  155. package/dist/service-client/src/protocol/client-protocol-wrapper.d.ts.map +1 -0
  156. package/dist/service-client/src/service-client.d.ts +41 -0
  157. package/dist/service-client/src/service-client.d.ts.map +1 -0
  158. package/dist/service-client/src/transport/service-transport.d.ts +24 -0
  159. package/dist/service-client/src/transport/service-transport.d.ts.map +1 -0
  160. package/dist/service-client/src/transport/socket-provider.d.ts +31 -0
  161. package/dist/service-client/src/transport/socket-provider.d.ts.map +1 -0
  162. package/dist/service-client/src/types/connection-config.d.ts +8 -0
  163. package/dist/service-client/src/types/connection-config.d.ts.map +1 -0
  164. package/dist/service-client/src/types/progress.types.d.ts +10 -0
  165. package/dist/service-client/src/types/progress.types.d.ts.map +1 -0
  166. package/dist/service-client/src/workers/client-protocol.worker.d.ts +2 -0
  167. package/dist/service-client/src/workers/client-protocol.worker.d.ts.map +1 -0
  168. package/dist/service-client.js +114 -0
  169. package/dist/service-client.js.map +7 -0
  170. package/dist/service-common/src/index.d.ts +8 -0
  171. package/dist/service-common/src/index.d.ts.map +1 -0
  172. package/dist/service-common/src/protocol/protocol.types.d.ts +100 -0
  173. package/dist/service-common/src/protocol/protocol.types.d.ts.map +1 -0
  174. package/dist/service-common/src/protocol/service-protocol.d.ts +63 -0
  175. package/dist/service-common/src/protocol/service-protocol.d.ts.map +1 -0
  176. package/dist/service-common/src/service-types/auto-update-service.types.d.ts +17 -0
  177. package/dist/service-common/src/service-types/auto-update-service.types.d.ts.map +1 -0
  178. package/dist/service-common/src/service-types/crypto-service.types.d.ts +22 -0
  179. package/dist/service-common/src/service-types/crypto-service.types.d.ts.map +1 -0
  180. package/dist/service-common/src/service-types/orm-service.types.d.ts +30 -0
  181. package/dist/service-common/src/service-types/orm-service.types.d.ts.map +1 -0
  182. package/dist/service-common/src/service-types/smtp-service.types.d.ts +55 -0
  183. package/dist/service-common/src/service-types/smtp-service.types.d.ts.map +1 -0
  184. package/dist/service-common/src/types.d.ts +43 -0
  185. package/dist/service-common/src/types.d.ts.map +1 -0
  186. package/dist/transport/service-transport.js +112 -0
  187. package/dist/transport/service-transport.js.map +7 -0
  188. package/dist/transport/socket-provider.js +170 -0
  189. package/dist/transport/socket-provider.js.map +7 -0
  190. package/dist/types/connection-config.js +1 -0
  191. package/dist/types/connection-config.js.map +7 -0
  192. package/dist/types/progress.types.js +1 -0
  193. package/dist/types/progress.types.js.map +7 -0
  194. package/dist/workers/client-protocol.worker.js +30 -0
  195. package/dist/workers/client-protocol.worker.js.map +7 -0
  196. package/package.json +26 -0
  197. package/src/features/event-client.ts +102 -0
  198. package/src/features/file-client.ts +56 -0
  199. package/src/features/orm/orm-client-connector.ts +50 -0
  200. package/src/features/orm/orm-client-db-context-executor.ts +98 -0
  201. package/src/features/orm/orm-connect-config.ts +11 -0
  202. package/src/index.ts +20 -0
  203. package/src/protocol/client-protocol-wrapper.ts +132 -0
  204. package/src/service-client.ts +157 -0
  205. package/src/transport/service-transport.ts +155 -0
  206. package/src/transport/socket-provider.ts +220 -0
  207. package/src/types/connection-config.ts +7 -0
  208. package/src/types/progress.types.ts +10 -0
  209. package/src/workers/client-protocol.worker.ts +54 -0
@@ -0,0 +1,56 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import type { ServiceUploadResult } from "@simplysm/service-common";
3
+
4
+ export class FileClient {
5
+ constructor(
6
+ private readonly _hostUrl: string,
7
+ private readonly _clientName: string,
8
+ ) {}
9
+
10
+ async download(relPath: string): Promise<Bytes> {
11
+ // URL 구성
12
+ const url = `${this._hostUrl}${relPath.startsWith("/") ? "" : "/"}${relPath}`;
13
+
14
+ const res = await fetch(url);
15
+ if (!res.ok) {
16
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
17
+ }
18
+
19
+ // ArrayBuffer -> Uint8Array
20
+ return new Uint8Array(await res.arrayBuffer());
21
+ }
22
+
23
+ async upload(
24
+ files: File[] | FileList | { name: string; data: BlobPart }[],
25
+ authToken: string,
26
+ ): Promise<ServiceUploadResult[]> {
27
+ const formData = new FormData();
28
+ const fileList = files instanceof FileList ? Array.from(files) : files;
29
+
30
+ for (const file of fileList) {
31
+ if ("data" in file) {
32
+ // 커스텀 객체 ({ name, data })
33
+ const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
34
+ formData.append("files", blob, file.name);
35
+ } else {
36
+ // 브라우저 File 객체
37
+ formData.append("files", file, file.name);
38
+ }
39
+ }
40
+
41
+ const res = await fetch(`${this._hostUrl}/upload`, {
42
+ method: "POST",
43
+ headers: {
44
+ "x-sd-client-name": this._clientName,
45
+ "Authorization": `Bearer ${authToken}`,
46
+ },
47
+ body: formData,
48
+ });
49
+
50
+ if (!res.ok) {
51
+ throw new Error(`Upload failed: ${res.statusText}`);
52
+ }
53
+
54
+ return res.json();
55
+ }
56
+ }
@@ -0,0 +1,50 @@
1
+ import { OrmClientDbContextExecutor } from "./orm-client-db-context-executor";
2
+ import type { OrmConnectConfig } from "./orm-connect-config";
3
+ import type { DbContext } from "@simplysm/orm-common";
4
+ import type { ServiceClient } from "../../service-client";
5
+
6
+ export class OrmClientConnector {
7
+ constructor(private readonly _serviceClient: ServiceClient) {}
8
+
9
+ async connect<T extends DbContext, R>(
10
+ config: OrmConnectConfig<T>,
11
+ callback: (conn: T) => Promise<R> | R,
12
+ ): Promise<R> {
13
+ const executor = new OrmClientDbContextExecutor(this._serviceClient, config.connOpt);
14
+ const info = await executor.getInfo();
15
+ const db = new config.dbContextType(executor, {
16
+ dialect: info.dialect,
17
+ database: config.dbContextOpt?.database ?? info.database,
18
+ schema: config.dbContextOpt?.schema ?? info.schema,
19
+ });
20
+ return db.connect(async () => {
21
+ try {
22
+ return await callback(db);
23
+ } catch (err) {
24
+ if (
25
+ err instanceof Error &&
26
+ (err.message.includes("a parent row: a foreign key constraint") ||
27
+ err.message.includes("conflicted with the REFERENCE"))
28
+ ) {
29
+ err.message = "경고! 연결된 작업에 의한 처리 거부. 후속작업 확인요망";
30
+ }
31
+
32
+ throw err;
33
+ }
34
+ });
35
+ }
36
+
37
+ async connectWithoutTransaction<T extends DbContext, R>(
38
+ config: OrmConnectConfig<T>,
39
+ callback: (conn: T) => Promise<R> | R,
40
+ ): Promise<R> {
41
+ const executor = new OrmClientDbContextExecutor(this._serviceClient, config.connOpt);
42
+ const info = await executor.getInfo();
43
+ const db = new config.dbContextType(executor, {
44
+ dialect: info.dialect,
45
+ database: config.dbContextOpt?.database ?? info.database,
46
+ schema: config.dbContextOpt?.schema ?? info.schema,
47
+ });
48
+ return db.connectWithoutTransaction(async () => callback(db));
49
+ }
50
+ }
@@ -0,0 +1,98 @@
1
+ import type {
2
+ DbContextExecutor,
3
+ ColumnMeta,
4
+ ResultMeta,
5
+ IsolationLevel,
6
+ Dialect,
7
+ QueryDef,
8
+ } from "@simplysm/orm-common";
9
+ import type { OrmService, DbConnOptions } from "@simplysm/service-common";
10
+ import type { ServiceClient } from "../../service-client";
11
+
12
+ export class OrmClientDbContextExecutor implements DbContextExecutor {
13
+ private _connId?: number;
14
+ private readonly _ormService: OrmService;
15
+
16
+ constructor(
17
+ private readonly _client: ServiceClient,
18
+ private readonly _opt: DbConnOptions & { configName: string },
19
+ ) {
20
+ // "SdOrmService" → "OrmService" 변경
21
+ this._ormService = _client.getService<OrmService>("OrmService");
22
+ }
23
+
24
+ async getInfo(): Promise<{
25
+ dialect: Dialect;
26
+ database?: string;
27
+ schema?: string;
28
+ }> {
29
+ return this._ormService.getInfo(this._opt);
30
+ }
31
+
32
+ async connect(): Promise<void> {
33
+ this._connId = await this._ormService.connect(this._opt);
34
+ }
35
+
36
+ async beginTransaction(isolationLevel?: IsolationLevel): Promise<void> {
37
+ if (this._connId === undefined) {
38
+ throw new Error("DB에 연결되어있지 않습니다.");
39
+ }
40
+
41
+ await this._ormService.beginTransaction(this._connId, isolationLevel);
42
+ }
43
+
44
+ async commitTransaction(): Promise<void> {
45
+ if (this._connId === undefined) {
46
+ throw new Error("DB에 연결되어있지 않습니다.");
47
+ }
48
+
49
+ await this._ormService.commitTransaction(this._connId);
50
+ }
51
+
52
+ async rollbackTransaction(): Promise<void> {
53
+ if (this._connId === undefined) {
54
+ throw new Error("DB에 연결되어있지 않습니다.");
55
+ }
56
+
57
+ await this._ormService.rollbackTransaction(this._connId);
58
+ }
59
+
60
+ async close(): Promise<void> {
61
+ if (this._connId === undefined) {
62
+ throw new Error("DB에 연결되어있지 않습니다.");
63
+ }
64
+
65
+ await this._ormService.close(this._connId);
66
+ }
67
+
68
+ async executeDefs<T = Record<string, unknown>>(
69
+ defs: QueryDef[],
70
+ options?: (ResultMeta | undefined)[],
71
+ ): Promise<T[][]> {
72
+ if (this._connId === undefined) {
73
+ throw new Error("DB에 연결되어있지 않습니다.");
74
+ }
75
+
76
+ return (await this._ormService.executeDefs(this._connId, defs, options)) as T[][];
77
+ }
78
+
79
+ async executeParametrized(query: string, params?: unknown[]): Promise<unknown[][]> {
80
+ if (this._connId === undefined) {
81
+ throw new Error("DB에 연결되어있지 않습니다.");
82
+ }
83
+
84
+ return this._ormService.executeParametrized(this._connId, query, params);
85
+ }
86
+
87
+ async bulkInsert(
88
+ tableName: string,
89
+ columnDefs: Record<string, ColumnMeta>,
90
+ records: Record<string, unknown>[],
91
+ ): Promise<void> {
92
+ if (this._connId === undefined) {
93
+ throw new Error("DB에 연결되어있지 않습니다.");
94
+ }
95
+
96
+ return this._ormService.bulkInsert(this._connId, tableName, columnDefs, records);
97
+ }
98
+ }
@@ -0,0 +1,11 @@
1
+ import type { Type } from "@simplysm/core-common";
2
+ import type { DbConnOptions } from "@simplysm/service-common";
3
+
4
+ export interface OrmConnectConfig<T> {
5
+ dbContextType: Type<T>;
6
+ connOpt: DbConnOptions & { configName: string };
7
+ dbContextOpt?: {
8
+ database: string;
9
+ schema: string;
10
+ };
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // Types
2
+ export * from "./types/connection-config";
3
+ export * from "./types/progress.types";
4
+
5
+ // Transport
6
+ export * from "./transport/socket-provider";
7
+ export * from "./transport/service-transport";
8
+
9
+ // Protocol
10
+ export * from "./protocol/client-protocol-wrapper";
11
+
12
+ // Features
13
+ export * from "./features/event-client";
14
+ export * from "./features/file-client";
15
+ export * from "./features/orm/orm-connect-config";
16
+ export * from "./features/orm/orm-client-connector";
17
+ export * from "./features/orm/orm-client-db-context-executor";
18
+
19
+ // Main
20
+ export * from "./service-client";
@@ -0,0 +1,132 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import { LazyGcMap, transferableDecode, Uuid } from "@simplysm/core-common";
3
+ import type { ServiceMessageDecodeResult, ServiceMessage } from "@simplysm/service-common";
4
+ import { ServiceProtocol } from "@simplysm/service-common";
5
+
6
+ export class ClientProtocolWrapper {
7
+ // 기준값: 30KB
8
+ private readonly _SIZE_THRESHOLD = 30 * 1024;
9
+
10
+ // 메인 스레드용 프로토콜 (가벼운 작업용)
11
+ private readonly _protocol = new ServiceProtocol();
12
+
13
+ // 워커 스레드 (무거운 작업용)
14
+ private static _worker?: Worker;
15
+ // 워커 요청 대기열 (Key: UUID)
16
+ private static readonly _workerResolvers = new LazyGcMap<
17
+ string,
18
+ { resolve: (res: unknown) => void; reject: (err: Error) => void }
19
+ >({
20
+ gcInterval: 5 * 1000, // 5초마다 만료 검사
21
+ expireTime: 60 * 1000, // 60초가 지나면 만료 (타임아웃)
22
+ onExpire: (key, item) => {
23
+ // 만료 시 reject 호출 (메모리 릭 방지 핵심)
24
+ item.reject(new Error(`Worker task timed out (uuid: ${key})`));
25
+ },
26
+ });
27
+
28
+ // Worker 사용 가능 여부 (브라우저 환경에서만 true)
29
+ private static _workerAvailable: boolean | undefined;
30
+
31
+ private static get workerAvailable(): boolean {
32
+ if (this._workerAvailable === undefined) {
33
+ this._workerAvailable = typeof Worker !== "undefined";
34
+ }
35
+ return this._workerAvailable;
36
+ }
37
+
38
+ private static get worker(): Worker | undefined {
39
+ if (!this.workerAvailable) {
40
+ return undefined;
41
+ }
42
+
43
+ if (!this._worker) {
44
+ // Vite/Esbuild/Webpack 등 모던 번들러는 이 문법을 통해 Worker를 별도 파일로 분리/로드함
45
+ // 주의: import.meta.resolve 대신 상대경로 사용 (Vite 호환)
46
+ this._worker = new Worker(new URL("../workers/client-protocol.worker.ts", import.meta.url), {
47
+ type: "module",
48
+ });
49
+
50
+ this._worker.onmessage = (event: MessageEvent) => {
51
+ const { id, type, result, error } = event.data as {
52
+ id: string;
53
+ type: "success" | "error";
54
+ result?: unknown;
55
+ error?: { message: string; stack?: string };
56
+ };
57
+
58
+ const resolver = this._workerResolvers.get(id);
59
+ if (resolver != null) {
60
+ if (type === "success") {
61
+ resolver.resolve(result);
62
+ } else {
63
+ const err = new Error(error?.message ?? "Unknown worker error");
64
+ err.stack = error?.stack;
65
+ resolver.reject(err);
66
+ }
67
+ this._workerResolvers.delete(id);
68
+ }
69
+ };
70
+ }
71
+ return this._worker;
72
+ }
73
+
74
+ /**
75
+ * Worker에 작업 위임 및 결과 대기
76
+ * 주의: workerAvailable이 true일 때만 호출해야 함
77
+ */
78
+ private async _runWorker(type: "encode" | "decode", data: unknown, transfer: Transferable[] = []): Promise<unknown> {
79
+ return new Promise((resolve, reject) => {
80
+ const id = Uuid.new().toString();
81
+
82
+ ClientProtocolWrapper._workerResolvers.set(id, { resolve, reject });
83
+ // workerAvailable 체크 후 호출되므로 worker는 항상 존재
84
+ ClientProtocolWrapper.worker!.postMessage({ id, type, data }, { transfer });
85
+ });
86
+ }
87
+
88
+ async encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }> {
89
+ // Worker가 없거나 작은 데이터는 메인 스레드에서 처리
90
+ if (!ClientProtocolWrapper.workerAvailable || !this._shouldUseWorkerForEncode(message)) {
91
+ return this._protocol.encode(uuid, message);
92
+ }
93
+
94
+ // [Worker]
95
+ // Encode는 객체를 보내야 하므로 Structured Clone이 발생함.
96
+ // 하지만 JSON.stringify 비용을 메인 스레드에서 제거하는 이득이 더 큼.
97
+ return (await this._runWorker("encode", { uuid, message })) as {
98
+ chunks: Bytes[];
99
+ totalSize: number;
100
+ };
101
+ }
102
+
103
+ async decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>> {
104
+ const totalSize = bytes.length;
105
+
106
+ // Worker가 없거나 작은 데이터는 메인 스레드에서 처리
107
+ if (!ClientProtocolWrapper.workerAvailable || totalSize <= this._SIZE_THRESHOLD) {
108
+ return this._protocol.decode(bytes);
109
+ }
110
+
111
+ // [Worker]
112
+ // Zero-Copy 전송 (buffer의 소유권이 Worker로 넘어감)
113
+ const rawResult = await this._runWorker("decode", bytes, [bytes.buffer]);
114
+
115
+ // Worker에서 온 결과(Plain Object)를 클래스 인스턴스(DateTime 등)로 복원
116
+ return transferableDecode(rawResult) as ServiceMessageDecodeResult<ServiceMessage>;
117
+ }
118
+
119
+ private _shouldUseWorkerForEncode(msg: ServiceMessage): boolean {
120
+ if (!("body" in msg)) return false;
121
+ const body = msg.body;
122
+
123
+ // Uint8Array가 있거나, 배열 길이가 길면 워커 사용
124
+ if (body instanceof Uint8Array) return true;
125
+ if (typeof body === "string" && body.length > this._SIZE_THRESHOLD) return true;
126
+ if (Array.isArray(body)) {
127
+ return body.length > 100 || (body.length > 0 && body[0] instanceof Uint8Array);
128
+ }
129
+
130
+ return false;
131
+ }
132
+ }
@@ -0,0 +1,157 @@
1
+ import { createConsola } from "consola";
2
+ import type { Type } from "@simplysm/core-common";
3
+ import { EventEmitter } from "@simplysm/core-common";
4
+ import type { ServiceEventListener } from "@simplysm/service-common";
5
+
6
+ import type { ServiceConnectionConfig } from "./types/connection-config";
7
+ import type { ServiceProgress, ServiceProgressState } from "./types/progress.types";
8
+ import { ServiceTransport } from "./transport/service-transport";
9
+ import { SocketProvider } from "./transport/socket-provider";
10
+ import { EventClient } from "./features/event-client";
11
+ import { FileClient } from "./features/file-client";
12
+
13
+ const logger = createConsola().withTag("service-client:ServiceClient");
14
+
15
+ interface ServiceClientEvents {
16
+ "request-progress": ServiceProgressState;
17
+ "response-progress": ServiceProgressState;
18
+ "state": "connected" | "closed" | "reconnecting";
19
+ "reload": Set<string>;
20
+ }
21
+
22
+ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
23
+ // 모듈들
24
+ private readonly _socket: SocketProvider;
25
+ private readonly _transport: ServiceTransport;
26
+ private readonly _eventClient: EventClient;
27
+ private readonly _fileClient: FileClient;
28
+
29
+ private _authToken?: string;
30
+
31
+ // 상태 접근자
32
+ get connected() {
33
+ return this._socket.connected;
34
+ }
35
+ get hostUrl() {
36
+ const hostProtocol = this.options.ssl ? "https" : "http";
37
+ return `${hostProtocol}://${this.options.host}:${this.options.port}`;
38
+ }
39
+
40
+ constructor(
41
+ public readonly name: string,
42
+ public readonly options: ServiceConnectionConfig,
43
+ ) {
44
+ super();
45
+
46
+ const wsProtocol = options.ssl ? "wss" : "ws";
47
+ const wsUrl = `${wsProtocol}://${options.host}:${options.port}/ws`;
48
+
49
+ // 모듈 초기화
50
+ this._socket = new SocketProvider(wsUrl, this.name, this.options.maxReconnectCount ?? 10);
51
+ this._transport = new ServiceTransport(this._socket);
52
+ this._eventClient = new EventClient(this._transport);
53
+ this._fileClient = new FileClient(this.hostUrl, this.name);
54
+
55
+ // 이벤트 바인딩
56
+ this._socket.on("state", async (state) => {
57
+ this.emit("state", state);
58
+
59
+ // 재연결 시 이벤트 리스너 자동 복구
60
+ if (state === "connected") {
61
+ try {
62
+ if (this._authToken != null) {
63
+ await this.auth(this._authToken); // 재인증
64
+ }
65
+ await this._eventClient.reRegisterAll();
66
+ } catch (err) {
67
+ logger.error("이벤트 리스너 복구 실패", err);
68
+ }
69
+ }
70
+ });
71
+
72
+ this._transport.on("reload", (changedFiles) => {
73
+ this.emit("reload", changedFiles);
74
+ });
75
+ }
76
+
77
+ // 타입 안전성을 위한 Proxy 생성 메소드
78
+ getService<T>(serviceName: string): RemoteService<T> {
79
+ return new Proxy({} as RemoteService<T>, {
80
+ get: (_target, prop) => {
81
+ const methodName = String(prop);
82
+ return async (...params: unknown[]) => {
83
+ return this.send(serviceName, methodName, params);
84
+ };
85
+ },
86
+ });
87
+ }
88
+
89
+ async connect(): Promise<void> {
90
+ await this._socket.connect();
91
+ }
92
+
93
+ async close(): Promise<void> {
94
+ await this._socket.close();
95
+ }
96
+
97
+ async send(serviceName: string, methodName: string, params: unknown[], progress?: ServiceProgress): Promise<unknown> {
98
+ return this._transport.send(
99
+ {
100
+ name: `${serviceName}.${methodName}`,
101
+ body: params,
102
+ },
103
+ {
104
+ request: (state) => {
105
+ this.emit("request-progress", state);
106
+ progress?.request?.(state);
107
+ },
108
+ response: (state) => {
109
+ this.emit("response-progress", state);
110
+ progress?.response?.(state);
111
+ },
112
+ },
113
+ );
114
+ }
115
+
116
+ async auth(token: string): Promise<void> {
117
+ await this._transport.send({ name: "auth", body: token });
118
+ this._authToken = token;
119
+ }
120
+
121
+ async addEventListener<T extends ServiceEventListener<unknown, unknown>>(
122
+ eventType: Type<T>,
123
+ info: T["$info"],
124
+ cb: (data: T["$data"]) => PromiseLike<void>,
125
+ ): Promise<string> {
126
+ if (!this.connected) throw new Error("서버와 연결되어있지 않습니다.");
127
+ return this._eventClient.addListener(eventType, info, cb);
128
+ }
129
+
130
+ async removeEventListener(key: string): Promise<void> {
131
+ await this._eventClient.removeListener(key);
132
+ }
133
+
134
+ async emitToServer<T extends ServiceEventListener<unknown, unknown>>(
135
+ eventType: Type<T>,
136
+ infoSelector: (item: T["$info"]) => boolean,
137
+ data: T["$data"],
138
+ ): Promise<void> {
139
+ await this._eventClient.emitToServer(eventType, infoSelector, data);
140
+ }
141
+
142
+ async uploadFile(files: File[] | FileList | { name: string; data: BlobPart }[]) {
143
+ if (this._authToken == null) {
144
+ throw new Error("인증 토큰이 없습니다. 파일 업로드를 위해서는 먼저 auth()를 호출하여 인증해야 합니다.");
145
+ }
146
+ return this._fileClient.upload(files, this._authToken);
147
+ }
148
+
149
+ async downloadFileBuffer(relPath: string) {
150
+ return this._fileClient.download(relPath);
151
+ }
152
+ }
153
+
154
+ // T의 모든 메소드 반환형을 Promise로 감싸주는 타입 변환기
155
+ export type RemoteService<T> = {
156
+ [K in keyof T]: T[K] extends (...args: infer P) => infer R ? (...args: P) => Promise<Awaited<R>> : never; // 함수가 아닌 프로퍼티는 안씀
157
+ };
@@ -0,0 +1,155 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import { EventEmitter, Uuid } from "@simplysm/core-common";
3
+ import type { ServiceErrorMessage, ServiceResponseMessage, ServiceClientMessage } from "@simplysm/service-common";
4
+ import { ClientProtocolWrapper } from "../protocol/client-protocol-wrapper";
5
+ import type { ServiceProgress } from "../types/progress.types";
6
+ import type { SocketProvider } from "./socket-provider";
7
+
8
+ interface ServiceTransportEvents {
9
+ reload: Set<string>;
10
+ event: { keys: string[]; data: unknown };
11
+ }
12
+
13
+ export class ServiceTransport extends EventEmitter<ServiceTransportEvents> {
14
+ private readonly _protocol = new ClientProtocolWrapper();
15
+
16
+ private readonly _pendingRequests = new Map<
17
+ string,
18
+ {
19
+ resolve: (msg: ServiceResponseMessage) => void;
20
+ reject: (err: Error) => void;
21
+ progress?: ServiceProgress;
22
+ }
23
+ >();
24
+
25
+ // 응답 progress의 totalSize 저장 (complete 시 100% emit용)
26
+ private readonly _responseProgressTotalSize = new Map<string, number>();
27
+
28
+ constructor(private readonly _socket: SocketProvider) {
29
+ super();
30
+
31
+ this._socket.on("message", this._onMessage.bind(this));
32
+
33
+ // 소켓이 끊기면 대기 중인 모든 요청을 에러 처리하여 메모리 해제
34
+ this._socket.on("state", (state) => {
35
+ if (state === "closed" || state === "reconnecting") {
36
+ this._cancelAllRequests("Socket connection lost");
37
+ }
38
+ });
39
+ }
40
+
41
+ async send(message: ServiceClientMessage, progress?: ServiceProgress): Promise<unknown> {
42
+ const uuid = Uuid.new().toString();
43
+
44
+ // 응답 대기 시작 (요청 보내기 전에 리스너를 먼저 등록해야 안전함)
45
+ const responsePromise = new Promise((resolve, reject) => {
46
+ this._pendingRequests.set(uuid, { resolve, reject, progress });
47
+ });
48
+
49
+ // 요청 전송
50
+ try {
51
+ const { chunks, totalSize } = await this._protocol.encode(uuid, message);
52
+
53
+ // 진행률 초기화
54
+ if (chunks.length > 1) {
55
+ progress?.request?.({
56
+ uuid,
57
+ totalSize,
58
+ completedSize: 0,
59
+ });
60
+ }
61
+
62
+ // 전송
63
+ for (const chunk of chunks) {
64
+ await this._socket.send(chunk);
65
+ }
66
+ } catch (err) {
67
+ // 전송 실패 시 즉시 정리
68
+ this._pendingRequests.get(uuid)?.reject(err as Error);
69
+ this._pendingRequests.delete(uuid);
70
+ throw err;
71
+ }
72
+
73
+ // 응답 결과 반환
74
+ return responsePromise;
75
+ }
76
+
77
+ private async _onMessage(buf: Bytes): Promise<void> {
78
+ const decoded = await this._protocol.decode(buf);
79
+
80
+ const listenerInfo = this._pendingRequests.get(decoded.uuid);
81
+
82
+ try {
83
+ if (decoded.type === "progress") {
84
+ // totalSize 기억 (complete 시 100% emit용)
85
+ this._responseProgressTotalSize.set(decoded.uuid, decoded.totalSize);
86
+
87
+ listenerInfo?.progress?.response?.({
88
+ uuid: decoded.uuid,
89
+ totalSize: decoded.totalSize,
90
+ completedSize: decoded.completedSize,
91
+ });
92
+ } else {
93
+ if (decoded.message.name === "progress") {
94
+ const body = decoded.message.body as { totalSize: number; completedSize: number };
95
+ listenerInfo?.progress?.request?.({
96
+ uuid: decoded.uuid,
97
+ totalSize: body.totalSize,
98
+ completedSize: body.completedSize,
99
+ });
100
+ } else if (decoded.message.name === "response") {
101
+ // split된 메시지였으면 100% progress emit
102
+ const totalSize = this._responseProgressTotalSize.get(decoded.uuid);
103
+ if (totalSize != null) {
104
+ this._responseProgressTotalSize.delete(decoded.uuid);
105
+ listenerInfo?.progress?.response?.({
106
+ uuid: decoded.uuid,
107
+ totalSize,
108
+ completedSize: totalSize,
109
+ });
110
+ }
111
+
112
+ // 응답을 받았으므로 Map에서 제거
113
+ this._pendingRequests.delete(decoded.uuid);
114
+
115
+ listenerInfo?.resolve(decoded.message.body as ServiceResponseMessage);
116
+ } else if (decoded.message.name === "error") {
117
+ // progress totalSize 정리
118
+ this._responseProgressTotalSize.delete(decoded.uuid);
119
+
120
+ // 에러를 받았으므로 Map에서 제거
121
+ this._pendingRequests.delete(decoded.uuid);
122
+
123
+ listenerInfo?.reject(this._toError(decoded.message.body));
124
+ } else if (decoded.message.name === "reload") {
125
+ const body = decoded.message.body as { clientName: string; changedFileSet: Set<string> };
126
+ if (this._socket.clientName === body.clientName) {
127
+ this.emit("reload", body.changedFileSet);
128
+ }
129
+ } else if (decoded.message.name === "evt:on") {
130
+ const body = decoded.message.body as { keys: string[]; data: unknown };
131
+ this.emit("event", { keys: body.keys, data: body.data });
132
+ } else {
133
+ throw new Error("요청이 잘 못 되었습니다.");
134
+ }
135
+ }
136
+ } catch (err) {
137
+ listenerInfo?.reject(err instanceof Error ? err : new Error(String(err)));
138
+ }
139
+ }
140
+
141
+ // 모든 대기 요청 취소 처리
142
+ private _cancelAllRequests(reason: string): void {
143
+ for (const listenerInfo of this._pendingRequests.values()) {
144
+ listenerInfo.reject(new Error(`Request canceled: ${reason}`));
145
+ }
146
+ this._pendingRequests.clear();
147
+ this._responseProgressTotalSize.clear();
148
+ }
149
+
150
+ private _toError(body: ServiceErrorMessage["body"]): Error {
151
+ let err = new Error(body.message);
152
+ err = Object.assign(err, body);
153
+ return err;
154
+ }
155
+ }