@simplysm/core-common 13.0.0-beta.2 → 13.0.0-beta.21
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/dist/common.types.js +4 -4
- package/dist/errors/argument-error.js +1 -1
- package/dist/errors/not-implemented-error.js +1 -1
- package/dist/errors/timeout-error.js +1 -1
- package/dist/extensions/arr-ext.helpers.js +4 -4
- package/dist/extensions/arr-ext.js +9 -9
- package/dist/features/debounce-queue.js +2 -2
- package/dist/features/serial-queue.js +3 -3
- package/dist/index.js +30 -30
- package/dist/types/date-only.js +2 -2
- package/dist/types/date-time.js +2 -2
- package/dist/types/time.js +2 -2
- package/dist/types/uuid.js +1 -1
- package/dist/utils/bytes.js +1 -1
- package/dist/utils/json.js +8 -8
- package/dist/utils/obj.js +5 -5
- package/dist/utils/primitive.js +5 -5
- package/dist/utils/transferable.js +4 -4
- package/dist/utils/wait.js +1 -1
- package/package.json +7 -4
- package/.cache/typecheck-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-node.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-node.tsbuildinfo +0 -1
- package/src/common.types.ts +0 -91
- package/src/env.ts +0 -11
- package/src/errors/argument-error.ts +0 -40
- package/src/errors/not-implemented-error.ts +0 -32
- package/src/errors/sd-error.ts +0 -53
- package/src/errors/timeout-error.ts +0 -36
- package/src/extensions/arr-ext.helpers.ts +0 -53
- package/src/extensions/arr-ext.ts +0 -777
- package/src/extensions/arr-ext.types.ts +0 -258
- package/src/extensions/map-ext.ts +0 -86
- package/src/extensions/set-ext.ts +0 -68
- package/src/features/debounce-queue.ts +0 -116
- package/src/features/event-emitter.ts +0 -112
- package/src/features/serial-queue.ts +0 -94
- package/src/globals.ts +0 -12
- package/src/index.ts +0 -55
- package/src/types/date-only.ts +0 -329
- package/src/types/date-time.ts +0 -294
- package/src/types/lazy-gc-map.ts +0 -244
- package/src/types/time.ts +0 -210
- package/src/types/uuid.ts +0 -113
- package/src/utils/bytes.ts +0 -160
- package/src/utils/date-format.ts +0 -239
- package/src/utils/json.ts +0 -230
- package/src/utils/num.ts +0 -97
- package/src/utils/obj.ts +0 -956
- package/src/utils/path.ts +0 -40
- package/src/utils/primitive.ts +0 -33
- package/src/utils/str.ts +0 -252
- package/src/utils/template-strings.ts +0 -132
- package/src/utils/transferable.ts +0 -269
- package/src/utils/wait.ts +0 -40
- package/src/utils/xml.ts +0 -105
- package/src/zip/sd-zip.ts +0 -218
- package/tests/errors/errors.spec.ts +0 -196
- package/tests/extensions/array-extension.spec.ts +0 -790
- package/tests/extensions/map-extension.spec.ts +0 -147
- package/tests/extensions/set-extension.spec.ts +0 -74
- package/tests/types/date-only.spec.ts +0 -636
- package/tests/types/date-time.spec.ts +0 -391
- package/tests/types/lazy-gc-map.spec.ts +0 -692
- package/tests/types/time.spec.ts +0 -559
- package/tests/types/types.spec.ts +0 -55
- package/tests/types/uuid.spec.ts +0 -91
- package/tests/utils/bytes-utils.spec.ts +0 -230
- package/tests/utils/date-format.spec.ts +0 -371
- package/tests/utils/debounce-queue.spec.ts +0 -272
- package/tests/utils/json.spec.ts +0 -475
- package/tests/utils/number.spec.ts +0 -184
- package/tests/utils/object.spec.ts +0 -827
- package/tests/utils/path.spec.ts +0 -78
- package/tests/utils/primitive.spec.ts +0 -55
- package/tests/utils/sd-event-emitter.spec.ts +0 -216
- package/tests/utils/serial-queue.spec.ts +0 -365
- package/tests/utils/string.spec.ts +0 -294
- package/tests/utils/template-strings.spec.ts +0 -96
- package/tests/utils/transferable.spec.ts +0 -698
- package/tests/utils/wait.spec.ts +0 -145
- package/tests/utils/xml.spec.ts +0 -146
- package/tests/zip/sd-zip.spec.ts +0 -234
package/src/types/date-time.ts
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { ArgumentError } from "../errors/argument-error";
|
|
2
|
-
import { convert12To24, formatDate, normalizeMonth } from "../utils/date-format";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 날짜시간 클래스 (불변)
|
|
6
|
-
*
|
|
7
|
-
* JavaScript Date 객체를 래핑하여 불변성과 편리한 API를 제공한다.
|
|
8
|
-
* 밀리초 단위까지 지원하며, 로컬 타임존을 기준으로 동작한다.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* const now = new DateTime();
|
|
12
|
-
* const specific = new DateTime(2025, 1, 15, 10, 30, 0);
|
|
13
|
-
* const parsed = DateTime.parse("2025-01-15 10:30:00");
|
|
14
|
-
*/
|
|
15
|
-
export class DateTime {
|
|
16
|
-
readonly date: Date;
|
|
17
|
-
|
|
18
|
-
/** 현재 시간으로 생성 */
|
|
19
|
-
constructor();
|
|
20
|
-
/** 연월일시분초밀리초로 생성 */
|
|
21
|
-
constructor(
|
|
22
|
-
year: number,
|
|
23
|
-
month: number,
|
|
24
|
-
day: number,
|
|
25
|
-
hour?: number,
|
|
26
|
-
minute?: number,
|
|
27
|
-
second?: number,
|
|
28
|
-
millisecond?: number,
|
|
29
|
-
);
|
|
30
|
-
/** tick (밀리초)으로 생성 */
|
|
31
|
-
constructor(tick: number);
|
|
32
|
-
/** Date 객체로 생성 */
|
|
33
|
-
constructor(date: Date);
|
|
34
|
-
constructor(
|
|
35
|
-
arg1?: number | Date,
|
|
36
|
-
arg2?: number,
|
|
37
|
-
arg3?: number,
|
|
38
|
-
arg4?: number,
|
|
39
|
-
arg5?: number,
|
|
40
|
-
arg6?: number,
|
|
41
|
-
arg7?: number,
|
|
42
|
-
) {
|
|
43
|
-
if (arg1 === undefined) {
|
|
44
|
-
this.date = new Date();
|
|
45
|
-
} else if (arg2 !== undefined && arg3 !== undefined) {
|
|
46
|
-
this.date = new Date(arg1 as number, arg2 - 1, arg3, arg4 ?? 0, arg5 ?? 0, arg6 ?? 0, arg7 ?? 0);
|
|
47
|
-
} else if (arg1 instanceof Date) {
|
|
48
|
-
this.date = new Date(arg1.getTime());
|
|
49
|
-
} else {
|
|
50
|
-
this.date = new Date(arg1);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 문자열을 파싱하여 DateTime 인스턴스를 생성
|
|
56
|
-
*
|
|
57
|
-
* @param str 날짜시간 문자열
|
|
58
|
-
* @returns 파싱된 DateTime 인스턴스
|
|
59
|
-
* @throws ArgumentError 지원하지 않는 형식인 경우
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* DateTime.parse("2025-01-15 10:30:00") // yyyy-MM-dd HH:mm:ss
|
|
63
|
-
* DateTime.parse("2025-01-15 10:30:00.123") // yyyy-MM-dd HH:mm:ss.fff
|
|
64
|
-
* DateTime.parse("20250115103000") // yyyyMMddHHmmss
|
|
65
|
-
* DateTime.parse("2025-01-15 오전 10:30:00") // yyyy-MM-dd 오전/오후 HH:mm:ss
|
|
66
|
-
* DateTime.parse("2025-01-15T10:30:00Z") // ISO 8601
|
|
67
|
-
*/
|
|
68
|
-
static parse(str: string): DateTime {
|
|
69
|
-
const parsedTick = Date.parse(str);
|
|
70
|
-
if (!Number.isNaN(parsedTick)) {
|
|
71
|
-
return new DateTime(parsedTick);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const match1 =
|
|
75
|
-
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) (오전|오후) ([0-9]{1,2}):([0-9]{2}):([0-9]{2})(\.([0-9]{1,3}))?$/.exec(str);
|
|
76
|
-
if (match1 != null) {
|
|
77
|
-
const rawHour = Number(match1[5]);
|
|
78
|
-
const isPM = match1[4] === "오후";
|
|
79
|
-
const hour = convert12To24(rawHour, isPM);
|
|
80
|
-
return new DateTime(
|
|
81
|
-
Number(match1[1]),
|
|
82
|
-
Number(match1[2]),
|
|
83
|
-
Number(match1[3]),
|
|
84
|
-
hour,
|
|
85
|
-
Number(match1[6]),
|
|
86
|
-
Number(match1[7]),
|
|
87
|
-
match1[9] ? Number(match1[9].padEnd(3, "0")) : undefined,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const match2 = /^[0-9]{14}$/.exec(str);
|
|
92
|
-
if (match2 != null) {
|
|
93
|
-
return new DateTime(
|
|
94
|
-
Number(str.substring(0, 4)),
|
|
95
|
-
Number(str.substring(4, 6)),
|
|
96
|
-
Number(str.substring(6, 8)),
|
|
97
|
-
Number(str.substring(8, 10)),
|
|
98
|
-
Number(str.substring(10, 12)),
|
|
99
|
-
Number(str.substring(12, 14)),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const match3 = /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{1,3}))?$/.exec(str);
|
|
104
|
-
if (match3 != null) {
|
|
105
|
-
return new DateTime(
|
|
106
|
-
Number(match3[1]),
|
|
107
|
-
Number(match3[2]),
|
|
108
|
-
Number(match3[3]),
|
|
109
|
-
Number(match3[4]),
|
|
110
|
-
Number(match3[5]),
|
|
111
|
-
Number(match3[6]),
|
|
112
|
-
match3[8] ? Number(match3[8].padEnd(3, "0")) : undefined,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
throw new ArgumentError(
|
|
117
|
-
`날짜시간 형식을 파싱할 수 없습니다. 지원 형식: 'yyyy-MM-dd HH:mm:ss', 'yyyyMMddHHmmss', 'yyyy-MM-dd 오전/오후 HH:mm:ss', ISO 8601`,
|
|
118
|
-
{ input: str },
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
//#region Getters (읽기 전용)
|
|
123
|
-
|
|
124
|
-
get year(): number {
|
|
125
|
-
return this.date.getFullYear();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
get month(): number {
|
|
129
|
-
return this.date.getMonth() + 1;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
get day(): number {
|
|
133
|
-
return this.date.getDate();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
get hour(): number {
|
|
137
|
-
return this.date.getHours();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
get minute(): number {
|
|
141
|
-
return this.date.getMinutes();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
get second(): number {
|
|
145
|
-
return this.date.getSeconds();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
get millisecond(): number {
|
|
149
|
-
return this.date.getMilliseconds();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
get tick(): number {
|
|
153
|
-
return this.date.getTime();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 요일 (일~토: 0~6) */
|
|
157
|
-
get dayOfWeek(): number {
|
|
158
|
-
return this.date.getDay();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
get timezoneOffsetMinutes(): number {
|
|
162
|
-
return -this.date.getTimezoneOffset();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 날짜시간 세팅이 제대로 되었는지 여부 */
|
|
166
|
-
get isValid(): boolean {
|
|
167
|
-
return this.date instanceof Date && !Number.isNaN(this.date.getTime());
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
//#endregion
|
|
171
|
-
|
|
172
|
-
//#region 불변 변환 메서드 (새 인스턴스 반환)
|
|
173
|
-
|
|
174
|
-
/** 지정된 연도로 새 인스턴스 반환 */
|
|
175
|
-
setYear(year: number): DateTime {
|
|
176
|
-
return new DateTime(year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 지정된 월로 새 DateTime 인스턴스를 반환
|
|
181
|
-
* @param month 설정할 월 (1-12, 범위 외 값은 연도 조정)
|
|
182
|
-
* @note 대상 월의 일수보다 현재 일자가 크면 해당 월의 마지막 날로 조정됨
|
|
183
|
-
* (예: 1월 31일에서 setMonth(2) → 2월 28일 또는 29일)
|
|
184
|
-
*/
|
|
185
|
-
setMonth(month: number): DateTime {
|
|
186
|
-
const normalized = normalizeMonth(this.year, month, this.day);
|
|
187
|
-
return new DateTime(
|
|
188
|
-
normalized.year,
|
|
189
|
-
normalized.month,
|
|
190
|
-
normalized.day,
|
|
191
|
-
this.hour,
|
|
192
|
-
this.minute,
|
|
193
|
-
this.second,
|
|
194
|
-
this.millisecond,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 지정된 일자로 새 DateTime 인스턴스를 반환
|
|
200
|
-
* @param day 설정할 일자
|
|
201
|
-
* @note 해당 월의 유효 범위를 벗어나는 일자는 JavaScript Date 기본 동작에 따라
|
|
202
|
-
* 자동으로 다음/이전 달로 조정됨 (예: 1월에 day=32 → 2월 1일)
|
|
203
|
-
*/
|
|
204
|
-
setDay(day: number): DateTime {
|
|
205
|
-
return new DateTime(this.year, this.month, day, this.hour, this.minute, this.second, this.millisecond);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** 지정된 시로 새 인스턴스 반환 */
|
|
209
|
-
setHour(hour: number): DateTime {
|
|
210
|
-
return new DateTime(this.year, this.month, this.day, hour, this.minute, this.second, this.millisecond);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** 지정된 분으로 새 인스턴스 반환 */
|
|
214
|
-
setMinute(minute: number): DateTime {
|
|
215
|
-
return new DateTime(this.year, this.month, this.day, this.hour, minute, this.second, this.millisecond);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** 지정된 초로 새 인스턴스 반환 */
|
|
219
|
-
setSecond(second: number): DateTime {
|
|
220
|
-
return new DateTime(this.year, this.month, this.day, this.hour, this.minute, second, this.millisecond);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** 지정된 밀리초로 새 인스턴스 반환 */
|
|
224
|
-
setMillisecond(millisecond: number): DateTime {
|
|
225
|
-
return new DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second, millisecond);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
//#endregion
|
|
229
|
-
|
|
230
|
-
//#region 산술 메서드 (새 인스턴스 반환)
|
|
231
|
-
|
|
232
|
-
/** 지정된 연수를 더한 새 인스턴스 반환 */
|
|
233
|
-
addYears(years: number): DateTime {
|
|
234
|
-
return this.setYear(this.year + years);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** 지정된 월수를 더한 새 인스턴스 반환 */
|
|
238
|
-
addMonths(months: number): DateTime {
|
|
239
|
-
return this.setMonth(this.month + months);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** 지정된 일수를 더한 새 인스턴스 반환 */
|
|
243
|
-
addDays(days: number): DateTime {
|
|
244
|
-
return new DateTime(this.tick + days * 24 * 60 * 60 * 1000);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** 지정된 시간을 더한 새 인스턴스 반환 */
|
|
248
|
-
addHours(hours: number): DateTime {
|
|
249
|
-
return new DateTime(this.tick + hours * 60 * 60 * 1000);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** 지정된 분을 더한 새 인스턴스 반환 */
|
|
253
|
-
addMinutes(minutes: number): DateTime {
|
|
254
|
-
return new DateTime(this.tick + minutes * 60 * 1000);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** 지정된 초를 더한 새 인스턴스 반환 */
|
|
258
|
-
addSeconds(seconds: number): DateTime {
|
|
259
|
-
return new DateTime(this.tick + seconds * 1000);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** 지정된 밀리초를 더한 새 인스턴스 반환 */
|
|
263
|
-
addMilliseconds(milliseconds: number): DateTime {
|
|
264
|
-
return new DateTime(this.tick + milliseconds);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
//#endregion
|
|
268
|
-
|
|
269
|
-
//#region 포맷팅
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* 지정된 포맷으로 문자열 변환
|
|
273
|
-
* @param format 포맷 문자열
|
|
274
|
-
* @see dtFormat 지원 포맷 문자열 참조
|
|
275
|
-
*/
|
|
276
|
-
toFormatString(formatStr: string): string {
|
|
277
|
-
return formatDate(formatStr, {
|
|
278
|
-
year: this.year,
|
|
279
|
-
month: this.month,
|
|
280
|
-
day: this.day,
|
|
281
|
-
hour: this.hour,
|
|
282
|
-
minute: this.minute,
|
|
283
|
-
second: this.second,
|
|
284
|
-
millisecond: this.millisecond,
|
|
285
|
-
timezoneOffsetMinutes: this.timezoneOffsetMinutes,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
toString(): string {
|
|
290
|
-
return this.toFormatString("yyyy-MM-ddTHH:mm:ss.fffzzz");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
//#endregion
|
|
294
|
-
}
|
package/src/types/lazy-gc-map.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { createConsola } from "consola";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 자동 만료 기능이 있는 Map
|
|
5
|
-
* LRU 방식으로 접근 시간 갱신, 일정 시간 미접근 시 자동 삭제
|
|
6
|
-
*
|
|
7
|
-
* @note 인스턴스 사용 후 반드시 dispose()를 호출하거나 using 문을 사용해야 함.
|
|
8
|
-
* 그렇지 않으면 GC 타이머가 계속 동작하여 메모리 누수 발생.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* // using 문 사용 (권장)
|
|
12
|
-
* using map = new LazyGcMap({ gcInterval: 10000, expireTime: 60000 });
|
|
13
|
-
*
|
|
14
|
-
* // 또는 명시적 dispose() 호출
|
|
15
|
-
* const map = new LazyGcMap({ gcInterval: 10000, expireTime: 60000 });
|
|
16
|
-
* try {
|
|
17
|
-
* // ... 사용
|
|
18
|
-
* } finally {
|
|
19
|
-
* map.dispose();
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
export class LazyGcMap<K, V> {
|
|
23
|
-
private static readonly _logger = createConsola().withTag("LazyGcMap");
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* dispose() 미호출 감지용 registry
|
|
27
|
-
* @note FinalizationRegistry는 Chrome 84+, Node 14.6+ 지원
|
|
28
|
-
* 미지원 환경에서는 경고 없이 동작하지만, dispose() 미호출 시 메모리 누수 가능
|
|
29
|
-
*/
|
|
30
|
-
private static readonly _registry =
|
|
31
|
-
typeof FinalizationRegistry !== "undefined"
|
|
32
|
-
? new FinalizationRegistry<string>((id) => {
|
|
33
|
-
LazyGcMap._logger.warn(`LazyGcMap(${id})이 dispose() 없이 가비지 수집됨. 메모리 누수 가능성 있음.`);
|
|
34
|
-
})
|
|
35
|
-
: undefined;
|
|
36
|
-
|
|
37
|
-
// 실제 데이터와 마지막 접근 시간을 함께 저장
|
|
38
|
-
private readonly _map = new Map<K, { value: V; lastAccess: number }>();
|
|
39
|
-
|
|
40
|
-
// GC 타이머
|
|
41
|
-
private _gcTimer?: ReturnType<typeof setInterval>;
|
|
42
|
-
// GC 실행 중 플래그 (중복 실행 방지)
|
|
43
|
-
private _isGcRunning = false;
|
|
44
|
-
// destroy 호출 여부
|
|
45
|
-
private _isDestroyed = false;
|
|
46
|
-
// 인스턴스 식별자 (경고 메시지용)
|
|
47
|
-
private readonly _instanceId = Math.random().toString(36).slice(2);
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @param _options 설정 옵션
|
|
51
|
-
* @param _options.gcInterval GC 주기 (밀리초). 기본값: expireTime의 1/10 (최소 1000ms)
|
|
52
|
-
* @param _options.expireTime 만료 시간 (밀리초). 마지막 접근 후 이 시간이 지나면 삭제됨. 예: 60000 (60초)
|
|
53
|
-
* @param _options.onExpire 만료 시 호출되는 콜백. 비동기 함수도 가능하며, 에러 발생 시 로깅 후 계속 진행됨
|
|
54
|
-
*/
|
|
55
|
-
constructor(
|
|
56
|
-
private readonly _options: {
|
|
57
|
-
gcInterval?: number;
|
|
58
|
-
expireTime: number;
|
|
59
|
-
onExpire?: (key: K, value: V) => void | Promise<void>;
|
|
60
|
-
},
|
|
61
|
-
) {
|
|
62
|
-
LazyGcMap._registry?.register(this, this._instanceId);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 저장된 항목 수 */
|
|
66
|
-
get size(): number {
|
|
67
|
-
return this._map.size;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 키 존재 여부 확인 (접근 시간 갱신 안함) */
|
|
71
|
-
has(key: K): boolean {
|
|
72
|
-
if (this._isDestroyed) return false;
|
|
73
|
-
return this._map.has(key);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** 값 조회 (접근 시간 갱신됨) */
|
|
77
|
-
get(key: K): V | undefined {
|
|
78
|
-
if (this._isDestroyed) return undefined;
|
|
79
|
-
const item = this._map.get(key);
|
|
80
|
-
if (item == null) return undefined;
|
|
81
|
-
|
|
82
|
-
// 접근 시 시간 갱신 (LRU)
|
|
83
|
-
item.lastAccess = Date.now();
|
|
84
|
-
return item.value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 값 저장 (접근 시간 설정 및 GC 타이머 시작) */
|
|
88
|
-
set(key: K, value: V): void {
|
|
89
|
-
if (this._isDestroyed) return;
|
|
90
|
-
this._map.set(key, { value, lastAccess: Date.now() });
|
|
91
|
-
// 데이터가 들어왔으므로 GC 타이머 가동
|
|
92
|
-
this._startGc();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** 항목 삭제 (비었으면 GC 타이머 중지) */
|
|
96
|
-
delete(key: K): boolean {
|
|
97
|
-
if (this._isDestroyed) return false;
|
|
98
|
-
const result = this._map.delete(key);
|
|
99
|
-
// 비었으면 타이머 중지
|
|
100
|
-
if (this._map.size === 0) {
|
|
101
|
-
this._stopGc();
|
|
102
|
-
}
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** 인스턴스 정리 (GC 타이머 중지 및 데이터 삭제) */
|
|
107
|
-
dispose(): void {
|
|
108
|
-
if (this._isDestroyed) return;
|
|
109
|
-
this._isDestroyed = true;
|
|
110
|
-
LazyGcMap._registry?.unregister(this);
|
|
111
|
-
|
|
112
|
-
this._map.clear();
|
|
113
|
-
this._stopGc();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** using 문 지원 */
|
|
117
|
-
[Symbol.dispose](): void {
|
|
118
|
-
this.dispose();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 모든 항목 삭제 (인스턴스는 계속 사용 가능)
|
|
123
|
-
*/
|
|
124
|
-
clear(): void {
|
|
125
|
-
if (this._isDestroyed) return;
|
|
126
|
-
this._map.clear();
|
|
127
|
-
this._stopGc();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 키에 해당하는 값을 반환하고, 없으면 factory로 생성 후 저장하여 반환
|
|
132
|
-
* @param key 조회할 키
|
|
133
|
-
* @param factory 키가 없을 때 값을 생성하는 함수
|
|
134
|
-
* @returns 기존 값 또는 새로 생성된 값
|
|
135
|
-
*/
|
|
136
|
-
getOrCreate(key: K, factory: () => V): V {
|
|
137
|
-
if (this._isDestroyed) {
|
|
138
|
-
throw new Error("LazyGcMap이 이미 dispose되었습니다.");
|
|
139
|
-
}
|
|
140
|
-
const item = this._map.get(key);
|
|
141
|
-
if (item == null) {
|
|
142
|
-
const value = factory();
|
|
143
|
-
this.set(key, value);
|
|
144
|
-
return value;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
item.lastAccess = Date.now();
|
|
148
|
-
return item.value;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** 값들만 순회 (Iterator) */
|
|
152
|
-
*values(): IterableIterator<V> {
|
|
153
|
-
if (this._isDestroyed) return;
|
|
154
|
-
for (const item of this._map.values()) {
|
|
155
|
-
yield item.value;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** 키들만 순회 (Iterator) */
|
|
160
|
-
*keys(): IterableIterator<K> {
|
|
161
|
-
if (this._isDestroyed) return;
|
|
162
|
-
yield* this._map.keys();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 엔트리 순회 (Iterator) */
|
|
166
|
-
*entries(): IterableIterator<[K, V]> {
|
|
167
|
-
if (this._isDestroyed) return;
|
|
168
|
-
for (const [key, item] of this._map.entries()) {
|
|
169
|
-
yield [key, item.value];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
//#region GC 로직
|
|
174
|
-
|
|
175
|
-
private _startGc(): void {
|
|
176
|
-
if (this._isDestroyed) return;
|
|
177
|
-
if (this._gcTimer != null) return;
|
|
178
|
-
|
|
179
|
-
const interval = this._options.gcInterval ?? Math.max(this._options.expireTime / 10, 1000);
|
|
180
|
-
this._gcTimer = setInterval(() => {
|
|
181
|
-
void this._runGc();
|
|
182
|
-
}, interval);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private async _runGc(): Promise<void> {
|
|
186
|
-
// 이미 실행 중이면 스킵 (onExpire 콜백이 오래 걸리는 경우 중복 실행 방지)
|
|
187
|
-
if (this._isGcRunning) return;
|
|
188
|
-
this._isGcRunning = true;
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const now = Date.now();
|
|
192
|
-
|
|
193
|
-
// 1. 만료된 항목 수집 (삭제 전)
|
|
194
|
-
const expiredEntries: { key: K; item: { value: V; lastAccess: number } }[] = [];
|
|
195
|
-
for (const [key, item] of this._map) {
|
|
196
|
-
if (now - item.lastAccess > this._options.expireTime) {
|
|
197
|
-
expiredEntries.push({ key, item });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 2. 각 항목에 대해 콜백 실행 후 삭제
|
|
202
|
-
for (const { key, item } of expiredEntries) {
|
|
203
|
-
// 콜백 실행 전 현재 상태 확인 (이미 다른 값으로 교체되었거나 삭제되었으면 스킵)
|
|
204
|
-
const currentItem = this._map.get(key);
|
|
205
|
-
if (currentItem !== item) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// 만료 콜백 실행
|
|
210
|
-
if (this._options.onExpire != null) {
|
|
211
|
-
try {
|
|
212
|
-
await this._options.onExpire(key, item.value);
|
|
213
|
-
} catch (err) {
|
|
214
|
-
LazyGcMap._logger.error("onExpire 콜백 에러", err);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 콜백 후 재등록 여부 확인
|
|
219
|
-
// 시나리오: onExpire 콜백에서 동일 키로 새 값을 set()한 경우,
|
|
220
|
-
// 새로 등록된 항목을 삭제하면 안 됨. item 참조가 같으면 재등록되지 않은 것이므로 삭제 진행.
|
|
221
|
-
const afterItem = this._map.get(key);
|
|
222
|
-
if (afterItem === item) {
|
|
223
|
-
this._map.delete(key);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// GC 후 비었으면 끄기
|
|
228
|
-
if (this._map.size === 0) {
|
|
229
|
-
this._stopGc();
|
|
230
|
-
}
|
|
231
|
-
} finally {
|
|
232
|
-
this._isGcRunning = false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private _stopGc(): void {
|
|
237
|
-
if (this._gcTimer != null) {
|
|
238
|
-
clearInterval(this._gcTimer);
|
|
239
|
-
this._gcTimer = undefined;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
//#endregion
|
|
244
|
-
}
|