@simplysm/core-common 13.0.96 → 13.0.98

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/docs/features.md CHANGED
@@ -1,343 +1,143 @@
1
- # 기능
1
+ # Features
2
2
 
3
- ## EventEmitter
3
+ Feature classes for async control flow and event handling.
4
4
 
5
- 타입 안전한 이벤트 에미터. `EventTarget` 기반으로 브라우저와 Node.js 모두에서 사용 가능하다.
5
+ Source: `src/features/*.ts`
6
6
 
7
- ```typescript
8
- import { EventEmitter } from "@simplysm/core-common";
9
-
10
- const emitter = new EventEmitter<{
11
- change: string;
12
- error: Error;
13
- ready: void;
14
- }>();
15
-
16
- // 리스너 등록 (동일 리스너 중복 등록 무시)
17
- emitter.on("change", (data) => { /* data: string */ });
18
-
19
- // 리스너 제거
20
- emitter.off("change", listener);
21
-
22
- // 이벤트 발생 (void 타입은 인자 없이 호출)
23
- emitter.emit("change", "updated");
24
- emitter.emit("ready");
25
-
26
- // 리스너 수 확인
27
- emitter.listenerCount("change");
7
+ ---
28
8
 
29
- // 정리 (Disposable 지원)
30
- emitter.dispose();
31
- // 또는
32
- using emitter2 = new EventEmitter<{ tick: number }>();
9
+ ## `EventEmitter`
10
+
11
+ Type-safe event emitter that can be used in both browsers and Node.js. Internally implemented using `EventTarget`. Supports the `using` statement (`Symbol.dispose`).
12
+
13
+ ```typescript
14
+ export class EventEmitter<
15
+ TEvents extends { [K in keyof TEvents]: unknown } = Record<string, unknown>,
16
+ > {
17
+ /**
18
+ * Register an event listener
19
+ * @note Duplicate registration of the same listener to the same event is ignored
20
+ */
21
+ on<TEventName extends keyof TEvents & string>(
22
+ type: TEventName,
23
+ listener: (data: TEvents[TEventName]) => void,
24
+ ): void;
25
+
26
+ /** Remove an event listener */
27
+ off<TEventName extends keyof TEvents & string>(
28
+ type: TEventName,
29
+ listener: (data: TEvents[TEventName]) => void,
30
+ ): void;
31
+
32
+ /**
33
+ * Emit an event
34
+ * @param args Event data (omitted if void type)
35
+ */
36
+ emit<TEventName extends keyof TEvents & string>(
37
+ type: TEventName,
38
+ ...args: TEvents[TEventName] extends void ? [] : [data: TEvents[TEventName]]
39
+ ): void;
40
+
41
+ /** Return the number of listeners for a specific event */
42
+ listenerCount<TEventName extends keyof TEvents & string>(type: TEventName): number;
43
+
44
+ /** Remove all event listeners */
45
+ dispose(): void;
46
+
47
+ [Symbol.dispose](): void;
48
+ }
33
49
  ```
34
50
 
35
- ### 상속 패턴
51
+ **Example:**
36
52
 
37
53
  ```typescript
38
54
  interface MyEvents {
39
55
  data: string;
40
56
  error: Error;
57
+ done: void;
41
58
  }
42
59
 
43
- class MyService extends EventEmitter<MyEvents> {
44
- process(): void {
45
- this.emit("data", "result");
46
- }
47
- }
48
- ```
49
-
50
- ---
51
-
52
- ## DebounceQueue
53
-
54
- 디바운스된 비동기 작업 실행. delay 내 마지막 요청만 실행된다.
55
-
56
- ```typescript
57
- import { DebounceQueue } from "@simplysm/core-common";
58
-
59
- const queue = new DebounceQueue(300); // 300ms 디바운스 (생략 시 즉시 실행)
60
-
61
- queue.on("error", (err) => { /* SdError */ });
62
-
63
- // 300ms 내 마지막 호출만 실행
64
- queue.run(async () => {
65
- await saveData();
66
- });
67
-
68
- queue.dispose();
69
- // 또는
70
- using queue2 = new DebounceQueue(100);
71
- ```
72
-
73
- 동작 방식:
74
- - delay 내 새 요청이 들어오면 이전 요청을 취소하고 새 요청만 실행
75
- - 실행 중 새 요청이 들어오면 실행 완료 후 **즉시** 새 요청 처리 (디바운스 delay 없이)
76
- - 에러 발생 시 "error" 이벤트 리스너가 있으면 이벤트 발생, 없으면 consola로 로깅
77
-
78
- ---
79
-
80
- ## SerialQueue
81
-
82
- 순차 비동기 작업 실행. 작업 간 선택적 간격(gap) 설정 가능. 에러가 발생해도 후속 작업은 계속 실행된다.
83
-
84
- ```typescript
85
- import { SerialQueue } from "@simplysm/core-common";
86
-
87
- const queue = new SerialQueue(100); // 작업 간 100ms 간격 (기본값: 0)
88
-
89
- queue.on("error", (err) => { /* SdError */ });
90
-
91
- queue.run(async () => await task1());
92
- queue.run(async () => await task2()); // task1 완료 + 100ms 후 실행
60
+ class MyEmitter extends EventEmitter<MyEvents> {}
93
61
 
94
- queue.dispose(); // 대기 중인 큐 비우기 (현재 실행 중인 작업은 완료)
95
- // 또는
96
- using queue2 = new SerialQueue();
62
+ const emitter = new MyEmitter();
63
+ emitter.on("data", (data) => console.log(data)); // data: string
64
+ emitter.emit("data", "hello");
65
+ emitter.emit("done"); // void type is called without arguments
97
66
  ```
98
67
 
99
68
  ---
100
69
 
101
- ## 에러 클래스
102
-
103
- ### SdError
104
-
105
- 에러 체인을 지원하는 기본 에러 클래스. ES2024 `cause` 속성을 활용한다. 메시지는 역순으로 `=>` 구분자로 조인된다.
106
-
107
- ```typescript
108
- import { SdError } from "@simplysm/core-common";
109
-
110
- // 메시지만
111
- throw new SdError("작업 실패");
112
-
113
- // 여러 메시지 (역순 조인)
114
- throw new SdError("하위 메시지", "상위 메시지");
115
- // 메시지: "상위 메시지 => 하위 메시지"
116
-
117
- // 원인 에러 래핑
118
- throw new SdError(originalError, "추가 컨텍스트");
119
- // 메시지: "추가 컨텍스트 => 원본 에러 메시지"
120
- // cause 속성에 originalError 저장
121
- // stack에 원인 에러 스택도 포함 (---- cause stack ---- 구분자)
122
- ```
123
-
124
- ### ArgumentError
125
-
126
- 잘못된 인자를 YAML 형식으로 표시하는 에러. `SdError`를 상속한다.
70
+ ## `DebounceQueue`
127
71
 
128
- ```typescript
129
- import { ArgumentError } from "@simplysm/core-common";
130
-
131
- throw new ArgumentError({ userId: -1, name: "" });
132
- // 메시지: "Invalid arguments.\n\nuserId: -1\nname: ''"
133
-
134
- throw new ArgumentError("유효하지 않은 입력", { userId: -1 });
135
- // 메시지: "유효하지 않은 입력\n\nuserId: -1"
136
- ```
137
-
138
- ### NotImplementedError
139
-
140
- 미구현 기능을 표시하는 에러. `SdError`를 상속한다.
141
-
142
- ```typescript
143
- import { NotImplementedError } from "@simplysm/core-common";
144
-
145
- throw new NotImplementedError();
146
- // 메시지: "Not implemented"
147
-
148
- throw new NotImplementedError("이 기능은 아직 구현되지 않았습니다");
149
- // 메시지: "Not implemented: 이 기능은 아직 구현되지 않았습니다"
150
- ```
72
+ Asynchronous debounce queue. When called multiple times within a short time, only the last request is executed and previous requests are ignored. Extends `EventEmitter<{ error: SdError }>`. Supports the `using` statement.
151
73
 
152
- ### TimeoutError
153
-
154
- 타임아웃 에러. `SdError`를 상속한다.
74
+ Requests added during execution are processed immediately after the current execution completes without debounce delay.
155
75
 
156
76
  ```typescript
157
- import { TimeoutError } from "@simplysm/core-common";
158
-
159
- throw new TimeoutError();
160
- // 메시지: "Waiting time exceeded"
77
+ export class DebounceQueue extends EventEmitter<{ error: SdError }> {
78
+ /**
79
+ * @param _delay Debounce delay time (milliseconds). If omitted, executes immediately (next event loop)
80
+ */
81
+ constructor(delay?: number);
161
82
 
162
- throw new TimeoutError(3);
163
- // 메시지: "Waiting time exceeded(3 attempts)"
83
+ /** Clean up pending tasks and timers */
84
+ dispose(): void;
164
85
 
165
- throw new TimeoutError(3, "API 응답 대기 초과");
166
- // 메시지: "Waiting time exceeded(3 attempts): API 응답 대기 초과"
167
- ```
86
+ [Symbol.dispose](): void;
168
87
 
169
- ---
170
-
171
- ## 배열 확장 메서드
172
-
173
- `import "@simplysm/core-common"` (side-effect import) `Array.prototype`에 추가되는 메서드.
174
-
175
- ### 요소 접근
176
-
177
- ```typescript
178
- [1, 2, 3].first(); // 1
179
- [1, 2, 3].first((x) => x > 1); // 2
180
- [1, 2, 3].last(); // 3
181
- [1, 2, 3].last((x) => x < 3); // 2
182
- [1, 2, 3].single((x) => x === 2); // 2 (복수 매칭 시 ArgumentError)
183
- ```
184
-
185
- ### 필터링
186
-
187
- ```typescript
188
- [1, null, 2, undefined].filterExists(); // [1, 2]
189
- items.ofType("string"); // string 타입만 필터
190
- items.ofType(MyClass); // MyClass 인스턴스만 필터
191
- await items.filterAsync(async (x) => check(x)); // 순차 비동기 필터
192
- ```
193
-
194
- ### 변환
195
-
196
- ```typescript
197
- items.mapMany(); // 2차원 -> 1차원 평탄화 (null 제거)
198
- items.mapMany((x) => x.children); // 매핑 후 평탄화
199
- await items.mapAsync(async (x) => transform(x)); // 순차 비동기 매핑
200
- await items.mapManyAsync(async (x) => getChildren(x)); // 순차 비동기 매핑 후 평탄화
201
- await items.parallelAsync(async (x) => process(x)); // Promise.all 병렬 처리
202
- ```
203
-
204
- ### 그룹화/매핑
205
-
206
- ```typescript
207
- items.groupBy((x) => x.category); // { key, values }[]
208
- items.groupBy((x) => x.cat, (x) => x.name); // 값 변환 포함
209
-
210
- items.toMap((x) => x.id); // Map<id, item> (중복 키 시 ArgumentError)
211
- items.toMap((x) => x.id, (x) => x.name); // Map<id, name>
212
- await items.toMapAsync(async (x) => x.id); // 비동기 키 매핑
213
-
214
- items.toArrayMap((x) => x.category); // Map<category, item[]>
215
- items.toArrayMap((x) => x.cat, (x) => x.name); // Map<category, name[]>
216
-
217
- items.toSetMap((x) => x.category); // Map<category, Set<item>>
218
- items.toMapValues((x) => x.cat, (arr) => arr.length); // Map<cat, count>
219
-
220
- items.toObject((x) => x.id); // Record<string, item>
221
- items.toObject((x) => x.id, (x) => x.name); // Record<string, name>
222
-
223
- items.toTree("id", "parentId"); // TreeArray<T>[] (parentId가 null인 항목이 루트)
224
- ```
225
-
226
- ### 정렬/고유
227
-
228
- ```typescript
229
- items.distinct(); // 고유 요소 (객체: 딥 비교 O(n^2))
230
- items.distinct({ matchAddress: true }); // 참조 비교 (Set 기반 O(n))
231
- items.distinct({ keyFn: (x) => x.id }); // 커스텀 키 (O(n))
232
-
233
- items.orderBy((x) => x.name); // 오름차순 (null/undefined 우선)
234
- items.orderByDesc((x) => x.score); // 내림차순
235
-
236
- items.shuffle(); // 랜덤 셔플 (Fisher-Yates)
237
- ```
238
-
239
- ### 집계
240
-
241
- ```typescript
242
- items.sum((x) => x.price); // 합계 (빈 배열이면 0)
243
- items.min((x) => x.date); // 최솟값 (number | string)
244
- items.max((x) => x.date); // 최댓값 (number | string)
88
+ /**
89
+ * Add a function to the queue
90
+ * If there was a previously added function, it will be replaced
91
+ */
92
+ run(fn: () => void | Promise<void>): void;
93
+ }
245
94
  ```
246
95
 
247
- ### 비교/병합
248
-
249
- ```typescript
250
- // 배열 diff (INSERT/DELETE/UPDATE)
251
- items.diffs(target);
252
- // 반환: { source, target }[]
253
- // source만 있으면 DELETE, target만 있으면 INSERT, 둘 다 있으면 UPDATE
254
-
255
- items.diffs(target, { keys: ["id"] }); // 키 기준 매칭
256
- items.diffs(target, { excludes: ["updatedAt"] }); // 비교 제외 키
257
-
258
- // 단방향 diff
259
- items.oneWayDiffs(orgItems, "id");
260
- items.oneWayDiffs(orgItems, (x) => x.id, { includeSame: true });
261
- // 반환: { type: "create"|"update"|"same", item, orgItem }[]
262
-
263
- // 배열 병합
264
- items.merge(target);
265
- items.merge(target, { keys: ["id"], excludes: ["updatedAt"] });
266
- ```
96
+ **Error handling:** If there are `"error"` event listeners, errors are emitted as events. Otherwise, errors are logged via `consola`.
267
97
 
268
- ### 원본 변경 메서드 (@mutates)
98
+ **Example:**
269
99
 
270
100
  ```typescript
271
- items.distinctThis(); // 원본 배열에서 중복 제거
272
- items.orderByThis((x) => x.name); // 원본 배열 오름차순 정렬
273
- items.orderByDescThis((x) => x.score); // 원본 배열 내림차순 정렬
274
- items.insert(2, newItem1, newItem2); // 인덱스 2에 삽입
275
- items.remove(item); // 참조 일치 항목 제거
276
- items.remove((x) => x.expired); // 조건 일치 항목 제거
277
- items.toggle(item); // 있으면 제거, 없으면 추가
278
- items.clear(); // 전체 제거
279
- ```
280
-
281
- ### 내보내는 타입
101
+ const queue = new DebounceQueue(300); // 300ms delay
102
+ queue.run(() => console.log("1")); // ignored
103
+ queue.run(() => console.log("2")); // ignored
104
+ queue.run(() => console.log("3")); // executed after 300ms
282
105
 
283
- ```typescript
284
- import type {
285
- ArrayDiffsResult, // { source, target } 유니온
286
- ArrayOneWayDiffResult, // { type, item, orgItem } 유니온
287
- TreeArray, // T & { children: TreeArray<T>[] }
288
- ComparableType, // string | number | boolean | DateTime | DateOnly | Time | undefined
289
- } from "@simplysm/core-common";
106
+ queue.on("error", (err) => console.error(err));
290
107
  ```
291
108
 
292
109
  ---
293
110
 
294
- ## Map 확장 메서드
295
-
296
- ```typescript
297
- const map = new Map<string, number[]>();
111
+ ## `SerialQueue`
298
112
 
299
- // 없으면 생성 (값 또는 팩토리 함수)
300
- map.getOrCreate("key", []); // 직접 값
301
- map.getOrCreate("key", () => []); // 팩토리 함수
302
- // 주의: V 타입이 함수이면 팩토리로 인식되므로 () => myFn 형태로 래핑 필요
303
-
304
- // 값 갱신 (키가 없으면 prev는 undefined)
305
- map.update("key", (prev) => [...(prev ?? []), 1]);
306
- ```
307
-
308
- ---
309
-
310
- ## Set 확장 메서드
113
+ Asynchronous serial queue. Functions added to the queue are executed sequentially. The next task starts only after one task completes. Subsequent tasks continue to execute even if an error occurs. Extends `EventEmitter<{ error: SdError }>`. Supports the `using` statement.
311
114
 
312
115
  ```typescript
313
- const set = new Set<string>();
116
+ export class SerialQueue extends EventEmitter<{ error: SdError }> {
117
+ /**
118
+ * @param gap Gap between each task (ms). Default: 0
119
+ */
120
+ constructor(gap?: number);
314
121
 
315
- set.adds("a", "b", "c"); // 다수 추가 (체이닝)
122
+ /** Clear pending queue (currently executing task will complete) */
123
+ dispose(): void;
316
124
 
317
- set.toggle("a"); // 있으면 제거, 없으면 추가
318
- set.toggle("b", "add"); // 강제 추가
319
- set.toggle("b", "del"); // 강제 제거
320
- ```
125
+ [Symbol.dispose](): void;
321
126
 
322
- ---
323
-
324
- ## 환경 변수
325
-
326
- ```typescript
327
- import { env } from "@simplysm/core-common";
328
-
329
- env.DEV; // boolean -- 개발 모드 여부 (process.env.DEV를 JSON.parse)
330
- env.VER; // string | undefined -- 버전 문자열 (process.env.VER)
127
+ /** Add a function to the queue and execute it */
128
+ run(fn: () => void | Promise<void>): void;
129
+ }
331
130
  ```
332
131
 
333
- ### `__DEV__` 글로벌 상수
132
+ **Error handling:** Same as `DebounceQueue` emitted as event if listeners exist, otherwise logged.
334
133
 
335
- 빌드 시점에 치환되는 글로벌 상수. 라이브러리 빌드에서는 치환되지 않고, 클라이언트/서버 빌드에서 `define: { '__DEV__': 'true/false' }`로 치환된다.
134
+ **Example:**
336
135
 
337
136
  ```typescript
338
- declare const __DEV__: boolean;
137
+ const queue = new SerialQueue();
138
+ queue.run(async () => { await fetch("/api/1"); });
139
+ queue.run(async () => { await fetch("/api/2"); }); // executed after 1 completes
140
+ queue.run(async () => { await fetch("/api/3"); }); // executed after 2 completes
339
141
 
340
- if (__DEV__) {
341
- // 개발 모드 전용 로직
342
- }
142
+ queue.on("error", (err) => console.error(err));
343
143
  ```