sonamu 0.9.4 → 0.9.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 (171) hide show
  1. package/dist/ai/providers/rtzr/utils.js +2 -2
  2. package/dist/api/config.d.ts +13 -2
  3. package/dist/api/config.d.ts.map +1 -1
  4. package/dist/api/config.js +1 -1
  5. package/dist/api/context.d.ts +17 -7
  6. package/dist/api/context.d.ts.map +1 -1
  7. package/dist/api/context.js +1 -1
  8. package/dist/api/decorators.d.ts +18 -0
  9. package/dist/api/decorators.d.ts.map +1 -1
  10. package/dist/api/decorators.js +54 -3
  11. package/dist/api/index.js +8 -3
  12. package/dist/api/sonamu.d.ts +24 -9
  13. package/dist/api/sonamu.d.ts.map +1 -1
  14. package/dist/api/sonamu.js +365 -79
  15. package/dist/api/websocket-helpers.d.ts +24 -0
  16. package/dist/api/websocket-helpers.d.ts.map +1 -0
  17. package/dist/api/websocket-helpers.js +77 -0
  18. package/dist/bin/cli.js +12 -4
  19. package/dist/database/upsert-builder.js +4 -4
  20. package/dist/dict/sonamu-dictionary.js +6 -6
  21. package/dist/entity/entity-manager.js +1 -1
  22. package/dist/entity/entity.js +3 -3
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +16 -4
  26. package/dist/migration/code-generation.d.ts.map +1 -1
  27. package/dist/migration/code-generation.js +8 -9
  28. package/dist/stream/index.d.ts +6 -0
  29. package/dist/stream/index.d.ts.map +1 -1
  30. package/dist/stream/index.js +13 -2
  31. package/dist/stream/ws-audience-resolver.d.ts +15 -0
  32. package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
  33. package/dist/stream/ws-audience-resolver.js +31 -0
  34. package/dist/stream/ws-audience.d.ts +28 -0
  35. package/dist/stream/ws-audience.d.ts.map +1 -0
  36. package/dist/stream/ws-audience.js +46 -0
  37. package/dist/stream/ws-cluster-bus.d.ts +23 -0
  38. package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
  39. package/dist/stream/ws-cluster-bus.js +18 -0
  40. package/dist/stream/ws-core.d.ts +15 -0
  41. package/dist/stream/ws-core.d.ts.map +1 -0
  42. package/dist/stream/ws-core.js +1 -0
  43. package/dist/stream/ws-delivery.d.ts +24 -0
  44. package/dist/stream/ws-delivery.d.ts.map +1 -0
  45. package/dist/stream/ws-delivery.js +103 -0
  46. package/dist/stream/ws-local-connection-store.d.ts +10 -0
  47. package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
  48. package/dist/stream/ws-local-connection-store.js +44 -0
  49. package/dist/stream/ws-presence-store.d.ts +61 -0
  50. package/dist/stream/ws-presence-store.d.ts.map +1 -0
  51. package/dist/stream/ws-presence-store.js +236 -0
  52. package/dist/stream/ws-registry.d.ts +42 -0
  53. package/dist/stream/ws-registry.d.ts.map +1 -0
  54. package/dist/stream/ws-registry.js +108 -0
  55. package/dist/stream/ws.d.ts +52 -0
  56. package/dist/stream/ws.d.ts.map +1 -0
  57. package/dist/stream/ws.js +397 -0
  58. package/dist/syncer/api-parser.d.ts.map +1 -1
  59. package/dist/syncer/api-parser.js +72 -2
  60. package/dist/syncer/checksum.d.ts.map +1 -1
  61. package/dist/syncer/checksum.js +13 -12
  62. package/dist/syncer/code-generator.d.ts.map +1 -1
  63. package/dist/syncer/code-generator.js +7 -4
  64. package/dist/syncer/event-batcher.d.ts +27 -0
  65. package/dist/syncer/event-batcher.d.ts.map +1 -0
  66. package/dist/syncer/event-batcher.js +69 -0
  67. package/dist/syncer/file-patterns.d.ts +48 -26
  68. package/dist/syncer/file-patterns.d.ts.map +1 -1
  69. package/dist/syncer/file-patterns.js +71 -23
  70. package/dist/syncer/file-tracking.d.ts +13 -0
  71. package/dist/syncer/file-tracking.d.ts.map +1 -0
  72. package/dist/syncer/file-tracking.js +33 -0
  73. package/dist/syncer/index.js +2 -2
  74. package/dist/syncer/module-loader.d.ts +2 -11
  75. package/dist/syncer/module-loader.d.ts.map +1 -1
  76. package/dist/syncer/module-loader.js +3 -3
  77. package/dist/syncer/syncer-actions.d.ts +39 -6
  78. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  79. package/dist/syncer/syncer-actions.js +125 -10
  80. package/dist/syncer/syncer.d.ts +33 -19
  81. package/dist/syncer/syncer.d.ts.map +1 -1
  82. package/dist/syncer/syncer.js +168 -168
  83. package/dist/syncer/watcher.d.ts +8 -0
  84. package/dist/syncer/watcher.d.ts.map +1 -0
  85. package/dist/syncer/watcher.js +105 -0
  86. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  87. package/dist/tasks/workflow-manager.js +2 -1
  88. package/dist/template/implementations/services.template.d.ts.map +1 -1
  89. package/dist/template/implementations/services.template.js +36 -1
  90. package/dist/testing/bootstrap.d.ts.map +1 -1
  91. package/dist/testing/bootstrap.js +8 -1
  92. package/dist/testing/data-explorer.d.ts.map +1 -1
  93. package/dist/testing/data-explorer.js +5 -3
  94. package/dist/testing/fixture-manager.js +1 -1
  95. package/dist/types/types.d.ts +2 -1
  96. package/dist/types/types.d.ts.map +1 -1
  97. package/dist/types/types.js +2 -2
  98. package/dist/ui/api.d.ts.map +1 -1
  99. package/dist/ui/api.js +4 -3
  100. package/dist/ui/cdd-service.js +1 -1
  101. package/dist/ui-web/assets/{index-C5KUjXm0.js → index-BmThfg-s.js} +39 -39
  102. package/dist/ui-web/assets/index-D4rYm-Xz.css +1 -0
  103. package/dist/ui-web/index.html +2 -2
  104. package/dist/utils/async-utils.d.ts +27 -3
  105. package/dist/utils/async-utils.d.ts.map +1 -1
  106. package/dist/utils/async-utils.js +56 -6
  107. package/dist/utils/formatter.d.ts +7 -1
  108. package/dist/utils/formatter.d.ts.map +1 -1
  109. package/dist/utils/formatter.js +95 -60
  110. package/dist/utils/fs-utils.d.ts +2 -0
  111. package/dist/utils/fs-utils.d.ts.map +1 -1
  112. package/dist/utils/fs-utils.js +10 -2
  113. package/dist/utils/process-utils.d.ts +6 -0
  114. package/dist/utils/process-utils.d.ts.map +1 -1
  115. package/dist/utils/process-utils.js +16 -3
  116. package/dist/utils/utils.d.ts +1 -0
  117. package/dist/utils/utils.d.ts.map +1 -1
  118. package/dist/utils/utils.js +2 -2
  119. package/package.json +7 -5
  120. package/src/ai/providers/rtzr/utils.ts +1 -1
  121. package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
  122. package/src/api/__tests__/websocket-context.types.test.ts +58 -0
  123. package/src/api/config.ts +28 -2
  124. package/src/api/context.ts +21 -7
  125. package/src/api/decorators.ts +101 -1
  126. package/src/api/sonamu.ts +529 -127
  127. package/src/api/websocket-helpers.ts +122 -0
  128. package/src/bin/cli.ts +10 -2
  129. package/src/database/upsert-builder.ts +3 -3
  130. package/src/dict/sonamu-dictionary.ts +3 -3
  131. package/src/entity/entity.ts +1 -1
  132. package/src/index.ts +6 -0
  133. package/src/migration/code-generation.ts +6 -11
  134. package/src/shared/app.shared.ts.txt +312 -4
  135. package/src/shared/web.shared.ts.txt +340 -4
  136. package/src/stream/__tests__/ws-contracts.test.ts +381 -0
  137. package/src/stream/__tests__/ws.test.ts +449 -0
  138. package/src/stream/index.ts +6 -0
  139. package/src/stream/ws-audience-resolver.ts +35 -0
  140. package/src/stream/ws-audience.ts +62 -0
  141. package/src/stream/ws-cluster-bus.ts +32 -0
  142. package/src/stream/ws-core.ts +16 -0
  143. package/src/stream/ws-delivery.ts +138 -0
  144. package/src/stream/ws-local-connection-store.ts +44 -0
  145. package/src/stream/ws-presence-store.ts +326 -0
  146. package/src/stream/ws-registry.ts +138 -0
  147. package/src/stream/ws.ts +591 -0
  148. package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
  149. package/src/syncer/api-parser.ts +112 -1
  150. package/src/syncer/checksum.ts +23 -29
  151. package/src/syncer/code-generator.ts +4 -1
  152. package/src/syncer/event-batcher.ts +72 -0
  153. package/src/syncer/file-patterns.ts +98 -30
  154. package/src/syncer/file-tracking.ts +27 -0
  155. package/src/syncer/module-loader.ts +5 -12
  156. package/src/syncer/syncer-actions.ts +179 -17
  157. package/src/syncer/syncer.ts +250 -287
  158. package/src/syncer/watcher.ts +128 -0
  159. package/src/tasks/workflow-manager.ts +1 -0
  160. package/src/template/__tests__/services.template.websocket.test.ts +79 -0
  161. package/src/template/implementations/services.template.ts +69 -0
  162. package/src/testing/bootstrap.ts +8 -1
  163. package/src/testing/data-explorer.ts +3 -2
  164. package/src/types/types.ts +20 -2
  165. package/src/ui/api.ts +10 -1
  166. package/src/utils/async-utils.ts +71 -4
  167. package/src/utils/formatter.ts +114 -75
  168. package/src/utils/fs-utils.ts +9 -0
  169. package/src/utils/process-utils.ts +17 -0
  170. package/src/utils/utils.ts +1 -1
  171. package/dist/ui-web/assets/index-Dr8pRJC_.css +0 -1
package/src/api/config.ts CHANGED
@@ -3,6 +3,7 @@ import { type FastifyCorsOptions } from "@fastify/cors";
3
3
  import { type FastifyFormbodyOptions } from "@fastify/formbody";
4
4
  import { type FastifyMultipartOptions } from "@fastify/multipart";
5
5
  import { type FastifyStaticOptions } from "@fastify/static";
6
+ import { type WebsocketPluginOptions } from "@fastify/websocket";
6
7
  import { type BetterAuthOptions } from "better-auth";
7
8
  import {
8
9
  type FastifyInstance,
@@ -18,9 +19,10 @@ import { type CacheConfig } from "../cache/types";
18
19
  import { type SonamuDBConfig } from "../database/db";
19
20
  import { type SonamuLoggingOptions } from "../logger/configure";
20
21
  import { type StorageConfig } from "../storage/types";
22
+ import { type WebSocketRuntimeOptions } from "../stream/ws";
21
23
  import { type WorkflowOptions } from "../tasks/workflow-manager";
22
24
  import { type Executable, type SonamuFastifyConfig } from "../types/types";
23
- import { type Context } from "./context";
25
+ import { type Context, type WebSocketContext } from "./context";
24
26
 
25
27
  export type DatabaseConfig = Omit<Knex.Config, "connection"> & {
26
28
  connection?: Knex.PgConnectionConfig;
@@ -180,6 +182,7 @@ export type SonamuServerOptions = {
180
182
  qs?: boolean | QsPluginOptions;
181
183
  sse?: boolean | SsePluginOptions;
182
184
  static?: boolean | FastifyStaticOptions;
185
+ ws?: boolean | WebsocketPluginOptions;
183
186
 
184
187
  custom?: (server: FastifyInstance) => void;
185
188
  };
@@ -200,6 +203,14 @@ export type SonamuServerOptions = {
200
203
  };
201
204
  };
202
205
 
206
+ /**
207
+ * WebSocket runtime 설정.
208
+ *
209
+ * 단일 인스턴스에서는 기본값으로 충분하며, 멀티 인스턴스/대규모 환경에서는
210
+ * presence store와 cluster bus를 여기서 주입합니다.
211
+ */
212
+ websocket?: WebSocketRuntimeOptions;
213
+
203
214
  apiConfig: SonamuFastifyConfig;
204
215
 
205
216
  /**
@@ -255,9 +266,24 @@ export type SonamuTaskOptions = {
255
266
  contextProvider: (
256
267
  defaultContext: Pick<
257
268
  Context,
258
- "reply" | "request" | "headers" | "createSSE" | "naiteStore" | "locale" | "user" | "session"
269
+ | "transport"
270
+ | "reply"
271
+ | "request"
272
+ | "headers"
273
+ | "createSSE"
274
+ | "naiteStore"
275
+ | "locale"
276
+ | "user"
277
+ | "session"
259
278
  >,
260
279
  ) => Context | Promise<Context>;
280
+ websocketContextProvider?: (
281
+ defaultContext: Pick<
282
+ WebSocketContext,
283
+ "transport" | "request" | "headers" | "ws" | "naiteStore" | "locale" | "user" | "session"
284
+ >,
285
+ request: FastifyRequest,
286
+ ) => WebSocketContext | Promise<WebSocketContext>;
261
287
  };
262
288
 
263
289
  export type SonamuSSROptions = {
@@ -14,24 +14,38 @@ import { type NaiteStore } from "../naite/naite";
14
14
  import { type BufferedFile } from "../storage/buffered-file";
15
15
  import { type UploadedFile } from "../storage/uploaded-file";
16
16
  import { type createSSEFactory } from "../stream/sse";
17
+ import { type WebSocketConnection, type WebSocketEventMap } from "../stream/ws";
17
18
 
18
19
  // oxlint-disable-next-line @typescript-eslint/no-empty-interface -- Context 확장 타입
19
20
  export interface ContextExtend {}
20
- export type Context = {
21
+ type BaseContext = {
21
22
  request: FastifyRequest;
22
- reply: FastifyReply<Server, IncomingMessage, ServerResponse, RouteGenericInterface>;
23
23
  headers: IncomingHttpHeaders;
24
- createSSE: <T extends ZodObject>(events: T) => ReturnType<typeof createSSEFactory<T>>;
25
24
  naiteStore: NaiteStore;
26
25
  /** 현재 요청의 locale */
27
26
  locale: string;
27
+ /** 현재 로그인한 사용자 (null이면 미인증) */
28
+ user: User | null;
29
+ /** 현재 세션 정보 (null이면 미인증) */
30
+ session: Session | null;
31
+ };
32
+
33
+ export type Context = BaseContext & {
34
+ transport: "http";
35
+ reply: FastifyReply<Server, IncomingMessage, ServerResponse, RouteGenericInterface>;
36
+ createSSE: <T extends ZodObject>(events: T) => ReturnType<typeof createSSEFactory<T>>;
28
37
  /** buffer 모드에서 업로드된 파일 */
29
38
  bufferedFiles?: BufferedFile[];
30
39
  /** stream 모드에서 업로드된 파일 */
31
40
  uploadedFiles?: UploadedFile[];
41
+ } & ContextExtend;
32
42
 
33
- /** 현재 로그인한 사용자 (null이면 미인증) */
34
- user: User | null;
35
- /** 현재 세션 정보 (null이면 미인증) */
36
- session: Session | null;
43
+ export type WebSocketContext<
44
+ TOut extends WebSocketEventMap = WebSocketEventMap,
45
+ TIn extends WebSocketEventMap = WebSocketEventMap,
46
+ > = BaseContext & {
47
+ transport: "ws";
48
+ ws: WebSocketConnection<TOut, TIn>;
37
49
  } & ContextExtend;
50
+
51
+ export type RuntimeContext = Context | WebSocketContext;
@@ -61,6 +61,24 @@ export type StreamDecoratorOptions = {
61
61
  guards?: GuardKey[];
62
62
  description?: string;
63
63
  };
64
+ export type WebSocketDecoratorOptions = {
65
+ // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- 이벤트 키별로 넘겨주는 값이므로 어떤 타입이든 상관없음
66
+ outEvents: z.ZodObject<any>;
67
+ // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- 이벤트 키별로 넘겨주는 값이므로 어떤 타입이든 상관없음
68
+ inEvents: z.ZodObject<any>;
69
+ path?: string;
70
+ resourceName?: string;
71
+ guards?: GuardKey[];
72
+ description?: string;
73
+ heartbeat?: number;
74
+ maxPayload?: number;
75
+ namespace?: string;
76
+ };
77
+ export type ResolvedWebSocketDecoratorOptions = WebSocketDecoratorOptions & {
78
+ // codegen이 타입 이름을 재사용할 수 있도록 syncer가 AST에서 보강하는 메타데이터
79
+ outEventsTypeRef?: ApiParamType.Ref;
80
+ inEventsTypeRef?: ApiParamType.Ref;
81
+ };
64
82
 
65
83
  type BufferUploadOptions = {
66
84
  consume?: "buffer";
@@ -85,6 +103,7 @@ export const registeredApis: {
85
103
  path: string;
86
104
  options: ApiDecoratorOptions;
87
105
  streamOptions?: StreamDecoratorOptions;
106
+ websocketOptions?: ResolvedWebSocketDecoratorOptions;
88
107
  uploadOptions?: UploadDecoratorOptions;
89
108
  }[] = [];
90
109
  export type ExtendedApi = {
@@ -93,6 +112,7 @@ export type ExtendedApi = {
93
112
  path: string;
94
113
  options: ApiDecoratorOptions;
95
114
  streamOptions?: StreamDecoratorOptions;
115
+ websocketOptions?: ResolvedWebSocketDecoratorOptions;
96
116
  uploadOptions?: UploadDecoratorOptions;
97
117
  typeParameters: ApiParamType.TypeParam[];
98
118
  parameters: ApiParam[];
@@ -103,6 +123,7 @@ type DecoratorTarget = { constructor: { name: string } };
103
123
  const DECORATOR_TYPES = {
104
124
  API: Symbol("api"),
105
125
  STREAM: Symbol("stream"),
126
+ WEBSOCKET: Symbol("websocket"),
106
127
  UPLOAD: Symbol("upload"),
107
128
  } as const;
108
129
 
@@ -110,7 +131,7 @@ function checkSingleDecorator(target: DecoratorTarget, propertyKey: string, deco
110
131
  const method = target[propertyKey as keyof typeof target] as { __decoratorType?: symbol };
111
132
  if (method?.__decoratorType && method?.__decoratorType !== decoratorType) {
112
133
  throw new Error(
113
- `@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @stream decorator on the same method.`,
134
+ `@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api, @stream, @websocket, or @upload decorator on the same method.`,
114
135
  );
115
136
  } else {
116
137
  method.__decoratorType = decoratorType;
@@ -278,6 +299,85 @@ export function stream(options: StreamDecoratorOptions) {
278
299
  };
279
300
  }
280
301
 
302
+ export function websocket(options: WebSocketDecoratorOptions) {
303
+ return (target: DecoratorTarget, propertyKey: string, descriptor: PropertyDescriptor) => {
304
+ const modelName = target.constructor.name.match(/(.+)Class$/)?.[1];
305
+ assert(
306
+ modelName,
307
+ `modelName is required on @websocket decorator on ${target.constructor.name}.${propertyKey}`,
308
+ );
309
+ const methodName = propertyKey;
310
+
311
+ checkSingleDecorator(target, propertyKey, DECORATOR_TYPES.WEBSOCKET);
312
+
313
+ const defaultPath = `/${inflection.camelize(
314
+ modelName.replace(/Model$/, "").replace(/Frame$/, ""),
315
+ true,
316
+ )}/${inflection.camelize(propertyKey, true)}`;
317
+ const path = options.path ?? defaultPath;
318
+ const { outEvents: _outEvents, inEvents: _inEvents, ...apiOptions } = options;
319
+ const optionsWithDefaults = {
320
+ ...apiOptions,
321
+ httpMethod: "GET" as HTTPMethods,
322
+ };
323
+
324
+ const existingApi = registeredApis.find(
325
+ (api) => api.modelName === modelName && api.methodName === methodName,
326
+ );
327
+ if (existingApi) {
328
+ assertNoConflictingPath("websocket", modelName, methodName, existingApi.path, path);
329
+ existingApi.path = path;
330
+
331
+ assertNoConflictingOptions(
332
+ "websocket",
333
+ modelName,
334
+ methodName,
335
+ existingApi.options,
336
+ optionsWithDefaults,
337
+ );
338
+ existingApi.options = {
339
+ ...existingApi.options,
340
+ ...optionsWithDefaults,
341
+ };
342
+
343
+ existingApi.websocketOptions = options;
344
+ } else {
345
+ registeredApis.push({
346
+ modelName,
347
+ methodName,
348
+ path,
349
+ options: optionsWithDefaults,
350
+ websocketOptions: options,
351
+ });
352
+ }
353
+
354
+ const originalMethod = descriptor.value;
355
+ descriptor.value = async function (this: BaseModelClass | BaseFrameClass, ...args: unknown[]) {
356
+ if (this instanceof BaseModelClass) {
357
+ getLogger(convertDomainToCategory(this.modelName, "model")).debug(
358
+ "websocket: {model}.{method}",
359
+ {
360
+ model: modelName,
361
+ method: methodName,
362
+ },
363
+ );
364
+ }
365
+
366
+ if (this instanceof BaseFrameClass) {
367
+ getLogger(convertDomainToCategory(this.frameName, "frame")).debug(
368
+ "websocket: {model}.{method}",
369
+ {
370
+ model: modelName,
371
+ method: methodName,
372
+ },
373
+ );
374
+ }
375
+
376
+ return originalMethod.apply(this, args);
377
+ };
378
+ };
379
+ }
380
+
281
381
  export function transactional(options: TransactionalOptions = {}) {
282
382
  const { isolation, readOnly, dbPreset = "w" } = options;
283
383