@simplysm/core-node 13.0.0-beta.5 → 13.0.0-beta.7

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.
@@ -1,176 +0,0 @@
1
- import { DebounceQueue } from "@simplysm/core-common";
2
- import * as chokidar from "chokidar";
3
- import { createConsola } from "consola";
4
- import type { EventName } from "chokidar/handler.js";
5
- import { type NormPath, pathNorm } from "../utils/path";
6
-
7
- //#region Types
8
-
9
- /**
10
- * 지원하는 파일 변경 이벤트 타입 목록.
11
- */
12
- const FS_WATCHER_EVENTS = ["add", "addDir", "change", "unlink", "unlinkDir"] as const;
13
-
14
- /**
15
- * 파일 변경 이벤트 타입.
16
- */
17
- export type FsWatcherEvent = (typeof FS_WATCHER_EVENTS)[number];
18
-
19
- /**
20
- * 파일 변경 정보.
21
- */
22
- export interface FsWatcherChangeInfo {
23
- /** 변경 이벤트 타입 */
24
- event: FsWatcherEvent;
25
- /** 변경된 파일/디렉토리 경로 (정규화됨) */
26
- path: NormPath;
27
- }
28
-
29
- //#endregion
30
-
31
- //#region FsWatcher
32
-
33
- /**
34
- * chokidar 기반 파일 시스템 감시 래퍼.
35
- * 짧은 시간 내 발생한 이벤트를 병합하여 콜백 호출.
36
- *
37
- * **주의**: chokidar의 `ignoreInitial` 옵션은 내부적으로 항상 `true`로 설정된다.
38
- * `options.ignoreInitial: false`를 전달하면 `onChange` 첫 호출 시 빈 배열로
39
- * 콜백이 호출되지만, 실제 초기 파일 목록은 포함되지 않는다.
40
- * 이는 이벤트 병합 로직과의 충돌을 방지하기 위한 의도된 동작이다.
41
- *
42
- * @example
43
- * const watcher = await FsWatcher.watch(["src/**\/*.ts"]);
44
- * watcher.onChange({ delay: 300 }, (changes) => {
45
- * for (const { path, event } of changes) {
46
- * console.log(`${event}: ${path}`);
47
- * }
48
- * });
49
- *
50
- * // 종료
51
- * await watcher.close();
52
- */
53
- export class FsWatcher {
54
- /**
55
- * 파일 감시 시작 (비동기).
56
- * ready 이벤트가 발생할 때까지 대기.
57
- *
58
- * @param paths - 감시할 파일/디렉토리 경로 또는 glob 패턴 배열
59
- * @param options - chokidar 옵션
60
- */
61
- static async watch(paths: string[], options?: chokidar.ChokidarOptions): Promise<FsWatcher> {
62
- return new Promise<FsWatcher>((resolve, reject) => {
63
- const watcher = new FsWatcher(paths, options);
64
- watcher._watcher.on("ready", () => {
65
- resolve(watcher);
66
- });
67
- watcher._watcher.on("error", reject);
68
- });
69
- }
70
-
71
- private readonly _watcher: chokidar.FSWatcher;
72
- private readonly _ignoreInitial: boolean = true;
73
- private readonly _debounceQueues: DebounceQueue[] = [];
74
-
75
- private readonly _logger = createConsola().withTag("sd-fs-watcher");
76
-
77
- private constructor(paths: string[], options?: chokidar.ChokidarOptions) {
78
- this._watcher = chokidar.watch(paths, {
79
- persistent: true,
80
- ...options,
81
- ignoreInitial: true,
82
- });
83
- this._ignoreInitial = options?.ignoreInitial ?? this._ignoreInitial;
84
-
85
- // 감시 중 발생하는 에러 로깅
86
- this._watcher.on("error", (err) => {
87
- this._logger.error("FsWatcher error:", err);
88
- });
89
- }
90
-
91
- /**
92
- * 파일 변경 이벤트 핸들러 등록.
93
- * 지정된 delay 시간 동안 이벤트를 모아서 한 번에 콜백 호출.
94
- *
95
- * @param opt.delay - 이벤트 병합 대기 시간 (ms)
96
- * @param cb - 변경 이벤트 콜백
97
- */
98
- onChange(opt: { delay?: number }, cb: (changeInfos: FsWatcherChangeInfo[]) => void | Promise<void>): this {
99
- const fnQ = new DebounceQueue(opt.delay);
100
- this._debounceQueues.push(fnQ);
101
-
102
- let changeInfoMap = new Map<string, EventName>();
103
-
104
- // ignoreInitial이 false면 초기에 빈 배열로 콜백 호출
105
- if (!this._ignoreInitial) {
106
- fnQ.run(async () => {
107
- await cb([]);
108
- });
109
- }
110
-
111
- this._watcher.on("all", (event, filePath) => {
112
- // 지원하는 이벤트만 처리
113
- if (!FS_WATCHER_EVENTS.includes(event as FsWatcherEvent)) return;
114
-
115
- /*
116
- * 이벤트 병합 전략:
117
- * 짧은 시간 내 같은 파일에 대해 여러 이벤트가 발생하면 최종 상태만 전달한다.
118
- * - add + change → add (생성 직후 수정은 생성으로 간주)
119
- * - add + unlink → 삭제 (생성 후 즉시 삭제는 변경 없음)
120
- * - unlink + add → add (삭제 후 재생성은 생성으로 간주)
121
- * - 그 외 → 최신 이벤트로 덮어씀
122
- */
123
- if (!changeInfoMap.has(filePath)) {
124
- changeInfoMap.set(filePath, event);
125
- }
126
- const prevEvent = changeInfoMap.get(filePath)!;
127
-
128
- if (prevEvent === "add" && event === "change") {
129
- // add 후 change → add 유지
130
- changeInfoMap.set(filePath, "add");
131
- } else if ((prevEvent === "add" && event === "unlink") || (prevEvent === "addDir" && event === "unlinkDir")) {
132
- // add 후 unlink → 변경 없음 (삭제)
133
- changeInfoMap.delete(filePath);
134
- } else if (prevEvent === "unlink" && (event === "add" || event === "change")) {
135
- // unlink 후 add/change → add (파일 재생성)
136
- changeInfoMap.set(filePath, "add");
137
- } else if (prevEvent === "unlinkDir" && event === "addDir") {
138
- // unlinkDir 후 addDir → addDir (디렉토리 재생성)
139
- changeInfoMap.set(filePath, "addDir");
140
- } else {
141
- changeInfoMap.set(filePath, event);
142
- }
143
-
144
- fnQ.run(async () => {
145
- if (changeInfoMap.size === 0) return;
146
-
147
- const currChangeInfoMap = changeInfoMap;
148
- changeInfoMap = new Map<string, EventName>();
149
-
150
- const changeInfos = Array.from(currChangeInfoMap.entries()).map(
151
- ([path, evt]): FsWatcherChangeInfo => ({
152
- path: pathNorm(path),
153
- event: evt as FsWatcherEvent,
154
- }),
155
- );
156
-
157
- await cb(changeInfos);
158
- });
159
- });
160
-
161
- return this;
162
- }
163
-
164
- /**
165
- * 파일 감시 종료.
166
- */
167
- async close(): Promise<void> {
168
- for (const q of this._debounceQueues) {
169
- q.dispose();
170
- }
171
- this._debounceQueues.length = 0;
172
- await this._watcher.close();
173
- }
174
- }
175
-
176
- //#endregion
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- // Utils
2
- export * from "./utils/fs";
3
- export * from "./utils/path";
4
-
5
- // Features
6
- export * from "./features/fs-watcher";
7
-
8
- // Worker
9
- export * from "./worker/types";
10
- export * from "./worker/worker";
11
- export * from "./worker/create-worker";