@jthong/util 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +7 -0
- package/dist/array/index.cjs +11 -0
- package/dist/array/index.cjs.map +1 -1
- package/dist/array/index.d.ts +91 -6
- package/dist/array/index.d.ts.map +1 -0
- package/dist/array/index.js +11 -1
- package/dist/array/index.js.map +1 -1
- package/dist/fn/index.cjs +54 -0
- package/dist/fn/index.cjs.map +1 -1
- package/dist/fn/index.d.ts +149 -5
- package/dist/fn/index.d.ts.map +1 -0
- package/dist/fn/index.js +51 -1
- package/dist/fn/index.js.map +1 -1
- package/dist/index.cjs +119 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -1
- package/dist/index.js.map +1 -1
- package/dist/object/index.cjs.map +1 -1
- package/dist/object/index.d.ts +44 -5
- package/dist/object/index.d.ts.map +1 -0
- package/dist/object/index.js.map +1 -1
- package/dist/string/index.cjs +11 -0
- package/dist/string/index.cjs.map +1 -1
- package/dist/string/index.d.ts +94 -6
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +10 -1
- package/dist/string/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/array/index.d.cts +0 -6
- package/dist/fn/index.d.cts +0 -5
- package/dist/index.d.cts +0 -4
- package/dist/object/index.d.cts +0 -5
- package/dist/string/index.d.cts +0 -6
package/README.md
CHANGED
|
@@ -27,11 +27,14 @@ import { debounce } from "@jthong/util/fn";
|
|
|
27
27
|
- `capitalize(str)` — 첫 글자 대문자
|
|
28
28
|
- `camelToKebab(str)` / `kebabToCamel(str)` — 케이스 변환
|
|
29
29
|
- `truncate(str, max, suffix?)` — 자르고 접미사 붙이기
|
|
30
|
+
- `formatNumber(n, locale?)` — 천 단위 콤마 (`1234567 → "1,234,567"`)
|
|
31
|
+
- `mask(str, { start?, end?, char? })` — 일부 마스킹 (`"01012345678" → "010****5678"`)
|
|
30
32
|
|
|
31
33
|
### `/array`
|
|
32
34
|
- `chunk(arr, size)` — n개씩 분할
|
|
33
35
|
- `unique(arr)` — 중복 제거
|
|
34
36
|
- `groupBy(arr, keyFn)` — 키별 그룹화
|
|
37
|
+
- `sortBy(arr, keyFn, order?)` — 키 기반 정렬 (asc/desc, 입력 불변)
|
|
35
38
|
- `range(start, end?, step?)` — 정수 범위
|
|
36
39
|
|
|
37
40
|
### `/object`
|
|
@@ -41,3 +44,7 @@ import { debounce } from "@jthong/util/fn";
|
|
|
41
44
|
### `/fn`
|
|
42
45
|
- `debounce(fn, ms)` / `throttle(fn, ms)`
|
|
43
46
|
- `sleep(ms)` — Promise 기반 대기
|
|
47
|
+
- `once(fn)` — 첫 호출 결과를 캐싱, 이후 동일 결과 반환
|
|
48
|
+
- `memoize(fn, keyFn?)` — 인자별 결과 캐싱
|
|
49
|
+
- `retry(fn, { retries?, delay?, backoff?, onError? })` — 지수 백오프 재시도
|
|
50
|
+
- `withTimeout(promise, ms, errorMessage?)` — Promise에 타임아웃 부여
|
package/dist/array/index.cjs
CHANGED
|
@@ -18,6 +18,16 @@ var groupBy = (arr, keyFn) => {
|
|
|
18
18
|
}
|
|
19
19
|
return result;
|
|
20
20
|
};
|
|
21
|
+
var sortBy = (arr, keyFn, order = "asc") => {
|
|
22
|
+
const sign = order === "asc" ? 1 : -1;
|
|
23
|
+
return [...arr].sort((a, b) => {
|
|
24
|
+
const ka = keyFn(a);
|
|
25
|
+
const kb = keyFn(b);
|
|
26
|
+
if (ka < kb) return -1 * sign;
|
|
27
|
+
if (ka > kb) return 1 * sign;
|
|
28
|
+
return 0;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
21
31
|
var range = (start, end, step = 1) => {
|
|
22
32
|
const [from, to] = end === void 0 ? [0, start] : [start, end];
|
|
23
33
|
const result = [];
|
|
@@ -30,6 +40,7 @@ var range = (start, end, step = 1) => {
|
|
|
30
40
|
exports.chunk = chunk;
|
|
31
41
|
exports.groupBy = groupBy;
|
|
32
42
|
exports.range = range;
|
|
43
|
+
exports.sortBy = sortBy;
|
|
33
44
|
exports.unique = unique;
|
|
34
45
|
//# sourceMappingURL=index.cjs.map
|
|
35
46
|
//# sourceMappingURL=index.cjs.map
|
package/dist/array/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/array/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../../src/array/index.ts"],"names":[],"mappings":";;;AAaO,IAAM,KAAA,GAAQ,CAAI,GAAA,EAAmB,IAAA,KAAwB;AAClE,EAAA,IAAI,IAAA,IAAQ,CAAA,EAAG,OAAO,EAAC;AACvB,EAAA,MAAM,SAAgB,EAAC;AACvB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,KAAK,IAAA,EAAM;AACzC,IAAA,MAAA,CAAO,KAAK,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAAA,EACpC;AACA,EAAA,OAAO,MAAA;AACT;AAcO,IAAM,MAAA,GAAS,CAAI,GAAA,KAA2B,KAAA,CAAM,KAAK,IAAI,GAAA,CAAI,GAAG,CAAC;AAsBrE,IAAM,OAAA,GAAU,CACrB,GAAA,EACA,KAAA,KACmB;AACnB,EAAA,MAAM,SAAS,EAAC;AAChB,EAAA,KAAA,MAAW,QAAQ,GAAA,EAAK;AACtB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAI,CAAA;AACtB,IAAA,CAAC,OAAO,GAAG,CAAA,KAAM,EAAC,EAAG,KAAK,IAAI,CAAA;AAAA,EAChC;AACA,EAAA,OAAO,MAAA;AACT;AAuBO,IAAM,MAAA,GAAS,CACpB,GAAA,EACA,KAAA,EACA,QAAwB,KAAA,KAChB;AACR,EAAA,MAAM,IAAA,GAAO,KAAA,KAAU,KAAA,GAAQ,CAAA,GAAI,EAAA;AACnC,EAAA,OAAO,CAAC,GAAG,GAAG,EAAE,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM;AAC7B,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,IAAI,EAAA,GAAK,EAAA,EAAI,OAAO,EAAA,GAAK,IAAA;AACzB,IAAA,IAAI,EAAA,GAAK,EAAA,EAAI,OAAO,CAAA,GAAI,IAAA;AACxB,IAAA,OAAO,CAAA;AAAA,EACT,CAAC,CAAA;AACH;AAqBO,IAAM,KAAA,GAAQ,CAAC,KAAA,EAAe,GAAA,EAAc,OAAO,CAAA,KAAgB;AACxE,EAAA,MAAM,CAAC,IAAA,EAAM,EAAE,CAAA,GAAI,GAAA,KAAQ,MAAA,GAAY,CAAC,CAAA,EAAG,KAAK,CAAA,GAAI,CAAC,KAAA,EAAO,GAAG,CAAA;AAC/D,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,KAAA,IAAS,CAAA,GAAI,MAAM,IAAA,GAAO,CAAA,GAAI,IAAI,EAAA,GAAK,CAAA,GAAI,EAAA,EAAI,CAAA,IAAK,IAAA,EAAM;AACxD,IAAA,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,EACf;AACA,EAAA,OAAO,MAAA;AACT","file":"index.cjs","sourcesContent":["/**\n * 배열을 지정한 크기로 분할합니다. 마지막 청크는 크기보다 작을 수 있습니다.\n *\n * @param arr - 분할할 배열\n * @param size - 각 청크의 크기 (양수)\n * @returns 청크들의 배열. `size <= 0`이면 빈 배열을 반환합니다.\n * @example\n * ```ts\n * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]\n * chunk([1, 2, 3], 5); // [[1, 2, 3]]\n * chunk([1, 2, 3], 0); // []\n * ```\n */\nexport const chunk = <T>(arr: readonly T[], size: number): T[][] => {\n if (size <= 0) return [];\n const result: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n};\n\n/**\n * 배열에서 중복 요소를 제거합니다. 등호 비교는 `Set`을 사용하므로 원시값/참조값 모두에 동작하지만,\n * 객체는 참조 동일성으로만 비교됩니다.\n *\n * @param arr - 원본 배열\n * @returns 중복이 제거된 새 배열 (입력 순서 유지)\n * @example\n * ```ts\n * unique([1, 2, 2, 3, 1]); // [1, 2, 3]\n * unique([\"a\", \"b\", \"a\"]); // [\"a\", \"b\"]\n * ```\n */\nexport const unique = <T>(arr: readonly T[]): T[] => Array.from(new Set(arr));\n\n/**\n * 키 함수가 반환한 값으로 배열을 그룹화합니다.\n *\n * @param arr - 그룹화할 배열\n * @param keyFn - 각 요소에서 그룹 키를 추출하는 함수\n * @returns 키별로 묶인 객체. 키는 `keyFn`의 반환값.\n * @example\n * ```ts\n * const users = [\n * { name: \"Alice\", role: \"admin\" },\n * { name: \"Bob\", role: \"user\" },\n * { name: \"Carol\", role: \"admin\" },\n * ];\n * groupBy(users, (u) => u.role);\n * // {\n * // admin: [{ name: \"Alice\", role: \"admin\" }, { name: \"Carol\", role: \"admin\" }],\n * // user: [{ name: \"Bob\", role: \"user\" }],\n * // }\n * ```\n */\nexport const groupBy = <T, K extends PropertyKey>(\n arr: readonly T[],\n keyFn: (item: T) => K,\n): Record<K, T[]> => {\n const result = {} as Record<K, T[]>;\n for (const item of arr) {\n const key = keyFn(item);\n (result[key] ??= []).push(item);\n }\n return result;\n};\n\ntype Comparable = string | number | bigint | boolean | Date;\n\n/**\n * 키 함수가 반환한 값을 기준으로 배열을 정렬합니다. **입력 배열은 변경되지 않습니다** (새 배열 반환).\n *\n * @param arr - 정렬할 배열\n * @param keyFn - 정렬 기준 값을 추출하는 함수 (`string` | `number` | `bigint` | `boolean` | `Date` 반환)\n * @param order - `\"asc\"` (오름차순, 기본값) 또는 `\"desc\"` (내림차순)\n * @returns 정렬된 새 배열\n * @example\n * ```ts\n * sortBy([{ n: 3 }, { n: 1 }, { n: 2 }], (i) => i.n);\n * // [{ n: 1 }, { n: 2 }, { n: 3 }]\n *\n * sortBy([\"banana\", \"apple\", \"cherry\"], (s) => s);\n * // [\"apple\", \"banana\", \"cherry\"]\n *\n * sortBy([1, 3, 2], (n) => n, \"desc\");\n * // [3, 2, 1]\n * ```\n */\nexport const sortBy = <T>(\n arr: readonly T[],\n keyFn: (item: T) => Comparable,\n order: \"asc\" | \"desc\" = \"asc\",\n): T[] => {\n const sign = order === \"asc\" ? 1 : -1;\n return [...arr].sort((a, b) => {\n const ka = keyFn(a);\n const kb = keyFn(b);\n if (ka < kb) return -1 * sign;\n if (ka > kb) return 1 * sign;\n return 0;\n });\n};\n\n/**\n * 정수 범위 배열을 생성합니다. Python의 `range()`와 같은 시맨틱.\n *\n * - 인자 1개: `[0, start)` 범위\n * - 인자 2개: `[start, end)` 범위\n * - 인자 3개: `[start, end)` 범위, `step` 간격\n *\n * @param start - 시작값 (또는 종료값, 인자 1개일 때)\n * @param end - 종료값 (배제, exclusive)\n * @param step - 증가폭. 기본값 `1`. 음수면 감소.\n * @returns 정수 배열\n * @example\n * ```ts\n * range(3); // [0, 1, 2]\n * range(2, 5); // [2, 3, 4]\n * range(0, 10, 3); // [0, 3, 6, 9]\n * range(5, 0, -1); // [5, 4, 3, 2, 1]\n * ```\n */\nexport const range = (start: number, end?: number, step = 1): number[] => {\n const [from, to] = end === undefined ? [0, start] : [start, end];\n const result: number[] = [];\n for (let i = from; step > 0 ? i < to : i > to; i += step) {\n result.push(i);\n }\n return result;\n};\n"]}
|
package/dist/array/index.d.ts
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 배열을 지정한 크기로 분할합니다. 마지막 청크는 크기보다 작을 수 있습니다.
|
|
3
|
+
*
|
|
4
|
+
* @param arr - 분할할 배열
|
|
5
|
+
* @param size - 각 청크의 크기 (양수)
|
|
6
|
+
* @returns 청크들의 배열. `size <= 0`이면 빈 배열을 반환합니다.
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
|
|
10
|
+
* chunk([1, 2, 3], 5); // [[1, 2, 3]]
|
|
11
|
+
* chunk([1, 2, 3], 0); // []
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare const chunk: <T>(arr: readonly T[], size: number) => T[][];
|
|
15
|
+
/**
|
|
16
|
+
* 배열에서 중복 요소를 제거합니다. 등호 비교는 `Set`을 사용하므로 원시값/참조값 모두에 동작하지만,
|
|
17
|
+
* 객체는 참조 동일성으로만 비교됩니다.
|
|
18
|
+
*
|
|
19
|
+
* @param arr - 원본 배열
|
|
20
|
+
* @returns 중복이 제거된 새 배열 (입력 순서 유지)
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* unique([1, 2, 2, 3, 1]); // [1, 2, 3]
|
|
24
|
+
* unique(["a", "b", "a"]); // ["a", "b"]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const unique: <T>(arr: readonly T[]) => T[];
|
|
28
|
+
/**
|
|
29
|
+
* 키 함수가 반환한 값으로 배열을 그룹화합니다.
|
|
30
|
+
*
|
|
31
|
+
* @param arr - 그룹화할 배열
|
|
32
|
+
* @param keyFn - 각 요소에서 그룹 키를 추출하는 함수
|
|
33
|
+
* @returns 키별로 묶인 객체. 키는 `keyFn`의 반환값.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const users = [
|
|
37
|
+
* { name: "Alice", role: "admin" },
|
|
38
|
+
* { name: "Bob", role: "user" },
|
|
39
|
+
* { name: "Carol", role: "admin" },
|
|
40
|
+
* ];
|
|
41
|
+
* groupBy(users, (u) => u.role);
|
|
42
|
+
* // {
|
|
43
|
+
* // admin: [{ name: "Alice", role: "admin" }, { name: "Carol", role: "admin" }],
|
|
44
|
+
* // user: [{ name: "Bob", role: "user" }],
|
|
45
|
+
* // }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare const groupBy: <T, K extends PropertyKey>(arr: readonly T[], keyFn: (item: T) => K) => Record<K, T[]>;
|
|
49
|
+
type Comparable = string | number | bigint | boolean | Date;
|
|
50
|
+
/**
|
|
51
|
+
* 키 함수가 반환한 값을 기준으로 배열을 정렬합니다. **입력 배열은 변경되지 않습니다** (새 배열 반환).
|
|
52
|
+
*
|
|
53
|
+
* @param arr - 정렬할 배열
|
|
54
|
+
* @param keyFn - 정렬 기준 값을 추출하는 함수 (`string` | `number` | `bigint` | `boolean` | `Date` 반환)
|
|
55
|
+
* @param order - `"asc"` (오름차순, 기본값) 또는 `"desc"` (내림차순)
|
|
56
|
+
* @returns 정렬된 새 배열
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* sortBy([{ n: 3 }, { n: 1 }, { n: 2 }], (i) => i.n);
|
|
60
|
+
* // [{ n: 1 }, { n: 2 }, { n: 3 }]
|
|
61
|
+
*
|
|
62
|
+
* sortBy(["banana", "apple", "cherry"], (s) => s);
|
|
63
|
+
* // ["apple", "banana", "cherry"]
|
|
64
|
+
*
|
|
65
|
+
* sortBy([1, 3, 2], (n) => n, "desc");
|
|
66
|
+
* // [3, 2, 1]
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare const sortBy: <T>(arr: readonly T[], keyFn: (item: T) => Comparable, order?: "asc" | "desc") => T[];
|
|
70
|
+
/**
|
|
71
|
+
* 정수 범위 배열을 생성합니다. Python의 `range()`와 같은 시맨틱.
|
|
72
|
+
*
|
|
73
|
+
* - 인자 1개: `[0, start)` 범위
|
|
74
|
+
* - 인자 2개: `[start, end)` 범위
|
|
75
|
+
* - 인자 3개: `[start, end)` 범위, `step` 간격
|
|
76
|
+
*
|
|
77
|
+
* @param start - 시작값 (또는 종료값, 인자 1개일 때)
|
|
78
|
+
* @param end - 종료값 (배제, exclusive)
|
|
79
|
+
* @param step - 증가폭. 기본값 `1`. 음수면 감소.
|
|
80
|
+
* @returns 정수 배열
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* range(3); // [0, 1, 2]
|
|
84
|
+
* range(2, 5); // [2, 3, 4]
|
|
85
|
+
* range(0, 10, 3); // [0, 3, 6, 9]
|
|
86
|
+
* range(5, 0, -1); // [5, 4, 3, 2, 1]
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare const range: (start: number, end?: number, step?: number) => number[];
|
|
90
|
+
export {};
|
|
91
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,EAAE,MAAM,MAAM,KAAG,CAAC,EAAE,EAO7D,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,KAAG,CAAC,EAA8B,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,SAAS,WAAW,EAC9C,KAAK,SAAS,CAAC,EAAE,EACjB,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KACpB,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAOf,CAAC;AAEF,KAAK,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE5D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,EACtB,KAAK,SAAS,CAAC,EAAE,EACjB,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,UAAU,EAC9B,QAAO,KAAK,GAAG,MAAc,KAC5B,CAAC,EASH,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,EAAE,aAAQ,KAAG,MAAM,EAOnE,CAAC"}
|
package/dist/array/index.js
CHANGED
|
@@ -16,6 +16,16 @@ var groupBy = (arr, keyFn) => {
|
|
|
16
16
|
}
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
|
+
var sortBy = (arr, keyFn, order = "asc") => {
|
|
20
|
+
const sign = order === "asc" ? 1 : -1;
|
|
21
|
+
return [...arr].sort((a, b) => {
|
|
22
|
+
const ka = keyFn(a);
|
|
23
|
+
const kb = keyFn(b);
|
|
24
|
+
if (ka < kb) return -1 * sign;
|
|
25
|
+
if (ka > kb) return 1 * sign;
|
|
26
|
+
return 0;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
19
29
|
var range = (start, end, step = 1) => {
|
|
20
30
|
const [from, to] = end === void 0 ? [0, start] : [start, end];
|
|
21
31
|
const result = [];
|
|
@@ -25,6 +35,6 @@ var range = (start, end, step = 1) => {
|
|
|
25
35
|
return result;
|
|
26
36
|
};
|
|
27
37
|
|
|
28
|
-
export { chunk, groupBy, range, unique };
|
|
38
|
+
export { chunk, groupBy, range, sortBy, unique };
|
|
29
39
|
//# sourceMappingURL=index.js.map
|
|
30
40
|
//# sourceMappingURL=index.js.map
|
package/dist/array/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/array/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/array/index.ts"],"names":[],"mappings":";AAaO,IAAM,KAAA,GAAQ,CAAI,GAAA,EAAmB,IAAA,KAAwB;AAClE,EAAA,IAAI,IAAA,IAAQ,CAAA,EAAG,OAAO,EAAC;AACvB,EAAA,MAAM,SAAgB,EAAC;AACvB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,KAAK,IAAA,EAAM;AACzC,IAAA,MAAA,CAAO,KAAK,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAAA,EACpC;AACA,EAAA,OAAO,MAAA;AACT;AAcO,IAAM,MAAA,GAAS,CAAI,GAAA,KAA2B,KAAA,CAAM,KAAK,IAAI,GAAA,CAAI,GAAG,CAAC;AAsBrE,IAAM,OAAA,GAAU,CACrB,GAAA,EACA,KAAA,KACmB;AACnB,EAAA,MAAM,SAAS,EAAC;AAChB,EAAA,KAAA,MAAW,QAAQ,GAAA,EAAK;AACtB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAI,CAAA;AACtB,IAAA,CAAC,OAAO,GAAG,CAAA,KAAM,EAAC,EAAG,KAAK,IAAI,CAAA;AAAA,EAChC;AACA,EAAA,OAAO,MAAA;AACT;AAuBO,IAAM,MAAA,GAAS,CACpB,GAAA,EACA,KAAA,EACA,QAAwB,KAAA,KAChB;AACR,EAAA,MAAM,IAAA,GAAO,KAAA,KAAU,KAAA,GAAQ,CAAA,GAAI,EAAA;AACnC,EAAA,OAAO,CAAC,GAAG,GAAG,EAAE,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM;AAC7B,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,IAAI,EAAA,GAAK,EAAA,EAAI,OAAO,EAAA,GAAK,IAAA;AACzB,IAAA,IAAI,EAAA,GAAK,EAAA,EAAI,OAAO,CAAA,GAAI,IAAA;AACxB,IAAA,OAAO,CAAA;AAAA,EACT,CAAC,CAAA;AACH;AAqBO,IAAM,KAAA,GAAQ,CAAC,KAAA,EAAe,GAAA,EAAc,OAAO,CAAA,KAAgB;AACxE,EAAA,MAAM,CAAC,IAAA,EAAM,EAAE,CAAA,GAAI,GAAA,KAAQ,MAAA,GAAY,CAAC,CAAA,EAAG,KAAK,CAAA,GAAI,CAAC,KAAA,EAAO,GAAG,CAAA;AAC/D,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,KAAA,IAAS,CAAA,GAAI,MAAM,IAAA,GAAO,CAAA,GAAI,IAAI,EAAA,GAAK,CAAA,GAAI,EAAA,EAAI,CAAA,IAAK,IAAA,EAAM;AACxD,IAAA,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,EACf;AACA,EAAA,OAAO,MAAA;AACT","file":"index.js","sourcesContent":["/**\n * 배열을 지정한 크기로 분할합니다. 마지막 청크는 크기보다 작을 수 있습니다.\n *\n * @param arr - 분할할 배열\n * @param size - 각 청크의 크기 (양수)\n * @returns 청크들의 배열. `size <= 0`이면 빈 배열을 반환합니다.\n * @example\n * ```ts\n * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]\n * chunk([1, 2, 3], 5); // [[1, 2, 3]]\n * chunk([1, 2, 3], 0); // []\n * ```\n */\nexport const chunk = <T>(arr: readonly T[], size: number): T[][] => {\n if (size <= 0) return [];\n const result: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n};\n\n/**\n * 배열에서 중복 요소를 제거합니다. 등호 비교는 `Set`을 사용하므로 원시값/참조값 모두에 동작하지만,\n * 객체는 참조 동일성으로만 비교됩니다.\n *\n * @param arr - 원본 배열\n * @returns 중복이 제거된 새 배열 (입력 순서 유지)\n * @example\n * ```ts\n * unique([1, 2, 2, 3, 1]); // [1, 2, 3]\n * unique([\"a\", \"b\", \"a\"]); // [\"a\", \"b\"]\n * ```\n */\nexport const unique = <T>(arr: readonly T[]): T[] => Array.from(new Set(arr));\n\n/**\n * 키 함수가 반환한 값으로 배열을 그룹화합니다.\n *\n * @param arr - 그룹화할 배열\n * @param keyFn - 각 요소에서 그룹 키를 추출하는 함수\n * @returns 키별로 묶인 객체. 키는 `keyFn`의 반환값.\n * @example\n * ```ts\n * const users = [\n * { name: \"Alice\", role: \"admin\" },\n * { name: \"Bob\", role: \"user\" },\n * { name: \"Carol\", role: \"admin\" },\n * ];\n * groupBy(users, (u) => u.role);\n * // {\n * // admin: [{ name: \"Alice\", role: \"admin\" }, { name: \"Carol\", role: \"admin\" }],\n * // user: [{ name: \"Bob\", role: \"user\" }],\n * // }\n * ```\n */\nexport const groupBy = <T, K extends PropertyKey>(\n arr: readonly T[],\n keyFn: (item: T) => K,\n): Record<K, T[]> => {\n const result = {} as Record<K, T[]>;\n for (const item of arr) {\n const key = keyFn(item);\n (result[key] ??= []).push(item);\n }\n return result;\n};\n\ntype Comparable = string | number | bigint | boolean | Date;\n\n/**\n * 키 함수가 반환한 값을 기준으로 배열을 정렬합니다. **입력 배열은 변경되지 않습니다** (새 배열 반환).\n *\n * @param arr - 정렬할 배열\n * @param keyFn - 정렬 기준 값을 추출하는 함수 (`string` | `number` | `bigint` | `boolean` | `Date` 반환)\n * @param order - `\"asc\"` (오름차순, 기본값) 또는 `\"desc\"` (내림차순)\n * @returns 정렬된 새 배열\n * @example\n * ```ts\n * sortBy([{ n: 3 }, { n: 1 }, { n: 2 }], (i) => i.n);\n * // [{ n: 1 }, { n: 2 }, { n: 3 }]\n *\n * sortBy([\"banana\", \"apple\", \"cherry\"], (s) => s);\n * // [\"apple\", \"banana\", \"cherry\"]\n *\n * sortBy([1, 3, 2], (n) => n, \"desc\");\n * // [3, 2, 1]\n * ```\n */\nexport const sortBy = <T>(\n arr: readonly T[],\n keyFn: (item: T) => Comparable,\n order: \"asc\" | \"desc\" = \"asc\",\n): T[] => {\n const sign = order === \"asc\" ? 1 : -1;\n return [...arr].sort((a, b) => {\n const ka = keyFn(a);\n const kb = keyFn(b);\n if (ka < kb) return -1 * sign;\n if (ka > kb) return 1 * sign;\n return 0;\n });\n};\n\n/**\n * 정수 범위 배열을 생성합니다. Python의 `range()`와 같은 시맨틱.\n *\n * - 인자 1개: `[0, start)` 범위\n * - 인자 2개: `[start, end)` 범위\n * - 인자 3개: `[start, end)` 범위, `step` 간격\n *\n * @param start - 시작값 (또는 종료값, 인자 1개일 때)\n * @param end - 종료값 (배제, exclusive)\n * @param step - 증가폭. 기본값 `1`. 음수면 감소.\n * @returns 정수 배열\n * @example\n * ```ts\n * range(3); // [0, 1, 2]\n * range(2, 5); // [2, 3, 4]\n * range(0, 10, 3); // [0, 3, 6, 9]\n * range(5, 0, -1); // [5, 4, 3, 2, 1]\n * ```\n */\nexport const range = (start: number, end?: number, step = 1): number[] => {\n const [from, to] = end === undefined ? [0, start] : [start, end];\n const result: number[] = [];\n for (let i = from; step > 0 ? i < to : i > to; i += step) {\n result.push(i);\n }\n return result;\n};\n"]}
|
package/dist/fn/index.cjs
CHANGED
|
@@ -19,9 +19,63 @@ var throttle = (fn, ms) => {
|
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
21
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
var once = (fn) => {
|
|
23
|
+
let called = false;
|
|
24
|
+
let result;
|
|
25
|
+
return (...args) => {
|
|
26
|
+
if (!called) {
|
|
27
|
+
called = true;
|
|
28
|
+
result = fn(...args);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
var memoize = (fn, keyFn = (...args) => JSON.stringify(args)) => {
|
|
34
|
+
const cache = /* @__PURE__ */ new Map();
|
|
35
|
+
return (...args) => {
|
|
36
|
+
const key = keyFn(...args);
|
|
37
|
+
const cached = cache.get(key);
|
|
38
|
+
if (cached !== void 0 || cache.has(key)) return cached;
|
|
39
|
+
const result = fn(...args);
|
|
40
|
+
cache.set(key, result);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
var retry = async (fn, opts = {}) => {
|
|
45
|
+
const { retries = 3, delay = 100, backoff = 2, onError } = opts;
|
|
46
|
+
let lastError;
|
|
47
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
lastError = err;
|
|
52
|
+
onError?.(err, attempt);
|
|
53
|
+
if (attempt < retries) {
|
|
54
|
+
await sleep(delay * Math.pow(backoff, attempt));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw lastError;
|
|
59
|
+
};
|
|
60
|
+
var withTimeout = (promise, ms, errorMessage = "Operation timed out") => {
|
|
61
|
+
let timeoutId;
|
|
62
|
+
const timeout = new Promise((_, reject) => {
|
|
63
|
+
timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);
|
|
64
|
+
});
|
|
65
|
+
return Promise.race([
|
|
66
|
+
promise.finally(() => {
|
|
67
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
68
|
+
}),
|
|
69
|
+
timeout
|
|
70
|
+
]);
|
|
71
|
+
};
|
|
22
72
|
|
|
23
73
|
exports.debounce = debounce;
|
|
74
|
+
exports.memoize = memoize;
|
|
75
|
+
exports.once = once;
|
|
76
|
+
exports.retry = retry;
|
|
24
77
|
exports.sleep = sleep;
|
|
25
78
|
exports.throttle = throttle;
|
|
79
|
+
exports.withTimeout = withTimeout;
|
|
26
80
|
//# sourceMappingURL=index.cjs.map
|
|
27
81
|
//# sourceMappingURL=index.cjs.map
|
package/dist/fn/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fn/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../../src/fn/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * 함수 호출을 지연시켜, 마지막 호출 후 `ms` 밀리초가 지나야 실제로 실행되도록 합니다.\n * 검색 입력, 윈도우 리사이즈 등 \"연속 입력이 멈춘 뒤\"에 한 번만 실행하고 싶을 때 사용합니다.\n *\n * @param fn - 디바운스할 함수\n * @param ms - 대기 시간 (밀리초)\n * @returns 디바운스가 적용된 새 함수\n * @example\n * ```ts\n * const onResize = debounce(() => console.log(\"resized\"), 200);\n * window.addEventListener(\"resize\", onResize);\n * // 사용자가 리사이즈를 멈춘 뒤 200ms 후에 한 번만 출력\n * ```\n */\nexport const debounce = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return (...args: Args) => {\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(() => fn(...args), ms);\n };\n};\n\n/**\n * `ms` 밀리초마다 최대 한 번씩만 함수가 실행되도록 제한합니다.\n * 스크롤/마우스 무브 등 너무 자주 호출되는 이벤트의 빈도를 줄일 때 사용합니다.\n *\n * @param fn - 스로틀할 함수\n * @param ms - 최소 호출 간격 (밀리초)\n * @returns 스로틀이 적용된 새 함수\n * @example\n * ```ts\n * const onScroll = throttle(() => console.log(\"scroll\"), 100);\n * window.addEventListener(\"scroll\", onScroll);\n * // 100ms에 최대 1번만 출력\n * ```\n */\nexport const throttle = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let last = 0;\n return (...args: Args) => {\n const now = Date.now();\n if (now - last >= ms) {\n last = now;\n fn(...args);\n }\n };\n};\n\n/**\n * 지정한 시간만큼 대기하는 Promise를 반환합니다.\n *\n * @param ms - 대기 시간 (밀리초)\n * @returns `ms` 이후 resolve되는 Promise\n * @example\n * ```ts\n * async function task() {\n * console.log(\"start\");\n * await sleep(1000);\n * console.log(\"1초 뒤\");\n * }\n * ```\n */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * 첫 호출의 결과를 저장하고, 이후 호출 시 인자에 관계없이 같은 결과를 반환합니다.\n * 초기화 함수나 싱글톤 생성 등 \"한 번만 실행되어야 하는 작업\"에 유용합니다.\n *\n * @param fn - 한 번만 실행할 함수\n * @returns 첫 호출 결과를 캐싱하는 래퍼 함수\n * @example\n * ```ts\n * const init = once(() => {\n * console.log(\"initialized\");\n * return { ready: true };\n * });\n *\n * init(); // \"initialized\" 출력 후 { ready: true } 반환\n * init(); // 출력 없음, 같은 객체 반환\n * ```\n */\nexport const once = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n): ((...args: Args) => R) => {\n let called = false;\n let result: R;\n return (...args: Args): R => {\n if (!called) {\n called = true;\n result = fn(...args);\n }\n return result;\n };\n};\n\n/**\n * 함수의 결과를 인자별로 캐싱합니다. 동일한 인자로 다시 호출되면 캐시된 결과를 반환합니다.\n *\n * 기본 캐시 키는 `JSON.stringify(args)`로 생성되므로 객체 인자도 값 비교가 가능합니다.\n * 필요하면 `keyFn`을 직접 지정해 키 생성 로직을 커스터마이즈하세요.\n *\n * @param fn - 결과를 캐싱할 함수\n * @param keyFn - 인자에서 캐시 키를 만드는 함수 (기본값: `JSON.stringify(args)`)\n * @returns 결과 캐싱이 적용된 래퍼 함수\n * @example\n * ```ts\n * const slow = memoize((n: number) => {\n * console.log(\"compute\", n);\n * return n * 2;\n * });\n *\n * slow(2); // \"compute 2\" 출력 후 4\n * slow(2); // 출력 없음, 4\n * slow(3); // \"compute 3\" 출력 후 6\n *\n * // 커스텀 키: 객체 id 기준 캐싱\n * const byId = memoize(\n * (user: { id: number; name: string }) => fetchProfile(user.id),\n * (user) => String(user.id),\n * );\n * ```\n */\nexport const memoize = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n keyFn: (...args: Args) => string = (...args) => JSON.stringify(args),\n): ((...args: Args) => R) => {\n const cache = new Map<string, R>();\n return (...args: Args): R => {\n const key = keyFn(...args);\n const cached = cache.get(key);\n if (cached !== undefined || cache.has(key)) return cached as R;\n const result = fn(...args);\n cache.set(key, result);\n return result;\n };\n};\n\n/**\n * {@link retry} 함수의 옵션.\n */\nexport interface RetryOptions {\n /** 추가 재시도 횟수. 총 시도 횟수는 `retries + 1`. 기본값 `3`. */\n retries?: number;\n /** 첫 재시도 전 대기 시간(ms). 기본값 `100`. */\n delay?: number;\n /** 매 재시도마다 대기 시간에 곱해질 배수 (지수 백오프). 기본값 `2`. */\n backoff?: number;\n /** 매 실패마다 호출되는 콜백. `attempt`는 0부터 시작하는 시도 인덱스. */\n onError?: (error: unknown, attempt: number) => void;\n}\n\n/**\n * 함수를 호출하다가 예외가 나면 지수 백오프로 재시도합니다. 동기/비동기 함수 모두 지원합니다.\n *\n * 모든 시도가 실패하면 마지막 에러를 throw합니다.\n *\n * @param fn - 실행할 함수 (동기 또는 비동기)\n * @param opts - 재시도 옵션 ({@link RetryOptions} 참고)\n * @returns 함수의 결과를 담은 Promise\n * @example\n * ```ts\n * // 기본 옵션: 최대 3회 재시도, 100ms부터 2배씩 백오프 (100, 200, 400ms)\n * const data = await retry(() => fetch(\"/api/data\").then((r) => r.json()));\n *\n * // 커스텀 옵션\n * await retry(unstableTask, {\n * retries: 5,\n * delay: 200,\n * backoff: 1.5,\n * onError: (err, attempt) => console.warn(`시도 ${attempt + 1} 실패`, err),\n * });\n * ```\n */\nexport const retry = async <T>(\n fn: () => Promise<T> | T,\n opts: RetryOptions = {},\n): Promise<T> => {\n const { retries = 3, delay = 100, backoff = 2, onError } = opts;\n let lastError: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n onError?.(err, attempt);\n if (attempt < retries) {\n await sleep(delay * Math.pow(backoff, attempt));\n }\n }\n }\n throw lastError;\n};\n\n/**\n * Promise에 타임아웃을 부여합니다. 지정한 시간 안에 settle되지 않으면 reject됩니다.\n *\n * 원본 Promise가 먼저 끝나면 내부 타이머는 자동으로 해제되므로 타이머 누수가 없습니다.\n *\n * @param promise - 타임아웃을 걸 Promise\n * @param ms - 제한 시간 (밀리초)\n * @param errorMessage - 타임아웃 시 던질 에러 메시지 (기본값 `\"Operation timed out\"`)\n * @returns 원본 결과 또는 타임아웃 에러로 settle되는 Promise\n * @example\n * ```ts\n * try {\n * const data = await withTimeout(fetch(\"/slow-api\"), 3000);\n * } catch (err) {\n * // 3초 안에 안 오면 여기로 떨어짐\n * }\n *\n * await withTimeout(longTask(), 5000, \"작업이 너무 오래 걸립니다\");\n * ```\n */\nexport const withTimeout = <T>(\n promise: Promise<T>,\n ms: number,\n errorMessage = \"Operation timed out\",\n): Promise<T> => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);\n });\n return Promise.race([\n promise.finally(() => {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }),\n timeout,\n ]);\n};\n"]}
|
package/dist/fn/index.d.ts
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 함수 호출을 지연시켜, 마지막 호출 후 `ms` 밀리초가 지나야 실제로 실행되도록 합니다.
|
|
3
|
+
* 검색 입력, 윈도우 리사이즈 등 "연속 입력이 멈춘 뒤"에 한 번만 실행하고 싶을 때 사용합니다.
|
|
4
|
+
*
|
|
5
|
+
* @param fn - 디바운스할 함수
|
|
6
|
+
* @param ms - 대기 시간 (밀리초)
|
|
7
|
+
* @returns 디바운스가 적용된 새 함수
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const onResize = debounce(() => console.log("resized"), 200);
|
|
11
|
+
* window.addEventListener("resize", onResize);
|
|
12
|
+
* // 사용자가 리사이즈를 멈춘 뒤 200ms 후에 한 번만 출력
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const debounce: <Args extends unknown[]>(fn: (...args: Args) => void, ms: number) => ((...args: Args) => void);
|
|
16
|
+
/**
|
|
17
|
+
* `ms` 밀리초마다 최대 한 번씩만 함수가 실행되도록 제한합니다.
|
|
18
|
+
* 스크롤/마우스 무브 등 너무 자주 호출되는 이벤트의 빈도를 줄일 때 사용합니다.
|
|
19
|
+
*
|
|
20
|
+
* @param fn - 스로틀할 함수
|
|
21
|
+
* @param ms - 최소 호출 간격 (밀리초)
|
|
22
|
+
* @returns 스로틀이 적용된 새 함수
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const onScroll = throttle(() => console.log("scroll"), 100);
|
|
26
|
+
* window.addEventListener("scroll", onScroll);
|
|
27
|
+
* // 100ms에 최대 1번만 출력
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare const throttle: <Args extends unknown[]>(fn: (...args: Args) => void, ms: number) => ((...args: Args) => void);
|
|
31
|
+
/**
|
|
32
|
+
* 지정한 시간만큼 대기하는 Promise를 반환합니다.
|
|
33
|
+
*
|
|
34
|
+
* @param ms - 대기 시간 (밀리초)
|
|
35
|
+
* @returns `ms` 이후 resolve되는 Promise
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* async function task() {
|
|
39
|
+
* console.log("start");
|
|
40
|
+
* await sleep(1000);
|
|
41
|
+
* console.log("1초 뒤");
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 첫 호출의 결과를 저장하고, 이후 호출 시 인자에 관계없이 같은 결과를 반환합니다.
|
|
48
|
+
* 초기화 함수나 싱글톤 생성 등 "한 번만 실행되어야 하는 작업"에 유용합니다.
|
|
49
|
+
*
|
|
50
|
+
* @param fn - 한 번만 실행할 함수
|
|
51
|
+
* @returns 첫 호출 결과를 캐싱하는 래퍼 함수
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const init = once(() => {
|
|
55
|
+
* console.log("initialized");
|
|
56
|
+
* return { ready: true };
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* init(); // "initialized" 출력 후 { ready: true } 반환
|
|
60
|
+
* init(); // 출력 없음, 같은 객체 반환
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const once: <Args extends unknown[], R>(fn: (...args: Args) => R) => ((...args: Args) => R);
|
|
64
|
+
/**
|
|
65
|
+
* 함수의 결과를 인자별로 캐싱합니다. 동일한 인자로 다시 호출되면 캐시된 결과를 반환합니다.
|
|
66
|
+
*
|
|
67
|
+
* 기본 캐시 키는 `JSON.stringify(args)`로 생성되므로 객체 인자도 값 비교가 가능합니다.
|
|
68
|
+
* 필요하면 `keyFn`을 직접 지정해 키 생성 로직을 커스터마이즈하세요.
|
|
69
|
+
*
|
|
70
|
+
* @param fn - 결과를 캐싱할 함수
|
|
71
|
+
* @param keyFn - 인자에서 캐시 키를 만드는 함수 (기본값: `JSON.stringify(args)`)
|
|
72
|
+
* @returns 결과 캐싱이 적용된 래퍼 함수
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const slow = memoize((n: number) => {
|
|
76
|
+
* console.log("compute", n);
|
|
77
|
+
* return n * 2;
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* slow(2); // "compute 2" 출력 후 4
|
|
81
|
+
* slow(2); // 출력 없음, 4
|
|
82
|
+
* slow(3); // "compute 3" 출력 후 6
|
|
83
|
+
*
|
|
84
|
+
* // 커스텀 키: 객체 id 기준 캐싱
|
|
85
|
+
* const byId = memoize(
|
|
86
|
+
* (user: { id: number; name: string }) => fetchProfile(user.id),
|
|
87
|
+
* (user) => String(user.id),
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare const memoize: <Args extends unknown[], R>(fn: (...args: Args) => R, keyFn?: (...args: Args) => string) => ((...args: Args) => R);
|
|
92
|
+
/**
|
|
93
|
+
* {@link retry} 함수의 옵션.
|
|
94
|
+
*/
|
|
95
|
+
export interface RetryOptions {
|
|
96
|
+
/** 추가 재시도 횟수. 총 시도 횟수는 `retries + 1`. 기본값 `3`. */
|
|
97
|
+
retries?: number;
|
|
98
|
+
/** 첫 재시도 전 대기 시간(ms). 기본값 `100`. */
|
|
99
|
+
delay?: number;
|
|
100
|
+
/** 매 재시도마다 대기 시간에 곱해질 배수 (지수 백오프). 기본값 `2`. */
|
|
101
|
+
backoff?: number;
|
|
102
|
+
/** 매 실패마다 호출되는 콜백. `attempt`는 0부터 시작하는 시도 인덱스. */
|
|
103
|
+
onError?: (error: unknown, attempt: number) => void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 함수를 호출하다가 예외가 나면 지수 백오프로 재시도합니다. 동기/비동기 함수 모두 지원합니다.
|
|
107
|
+
*
|
|
108
|
+
* 모든 시도가 실패하면 마지막 에러를 throw합니다.
|
|
109
|
+
*
|
|
110
|
+
* @param fn - 실행할 함수 (동기 또는 비동기)
|
|
111
|
+
* @param opts - 재시도 옵션 ({@link RetryOptions} 참고)
|
|
112
|
+
* @returns 함수의 결과를 담은 Promise
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* // 기본 옵션: 최대 3회 재시도, 100ms부터 2배씩 백오프 (100, 200, 400ms)
|
|
116
|
+
* const data = await retry(() => fetch("/api/data").then((r) => r.json()));
|
|
117
|
+
*
|
|
118
|
+
* // 커스텀 옵션
|
|
119
|
+
* await retry(unstableTask, {
|
|
120
|
+
* retries: 5,
|
|
121
|
+
* delay: 200,
|
|
122
|
+
* backoff: 1.5,
|
|
123
|
+
* onError: (err, attempt) => console.warn(`시도 ${attempt + 1} 실패`, err),
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare const retry: <T>(fn: () => Promise<T> | T, opts?: RetryOptions) => Promise<T>;
|
|
128
|
+
/**
|
|
129
|
+
* Promise에 타임아웃을 부여합니다. 지정한 시간 안에 settle되지 않으면 reject됩니다.
|
|
130
|
+
*
|
|
131
|
+
* 원본 Promise가 먼저 끝나면 내부 타이머는 자동으로 해제되므로 타이머 누수가 없습니다.
|
|
132
|
+
*
|
|
133
|
+
* @param promise - 타임아웃을 걸 Promise
|
|
134
|
+
* @param ms - 제한 시간 (밀리초)
|
|
135
|
+
* @param errorMessage - 타임아웃 시 던질 에러 메시지 (기본값 `"Operation timed out"`)
|
|
136
|
+
* @returns 원본 결과 또는 타임아웃 에러로 settle되는 Promise
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* try {
|
|
140
|
+
* const data = await withTimeout(fetch("/slow-api"), 3000);
|
|
141
|
+
* } catch (err) {
|
|
142
|
+
* // 3초 안에 안 오면 여기로 떨어짐
|
|
143
|
+
* }
|
|
144
|
+
*
|
|
145
|
+
* await withTimeout(longTask(), 5000, "작업이 너무 오래 걸립니다");
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export declare const withTimeout: <T>(promise: Promise<T>, ms: number, errorMessage?: string) => Promise<T>;
|
|
149
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fn/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,GAAI,IAAI,SAAS,OAAO,EAAE,EAC7C,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,MAAM,KACT,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,CAM1B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,GAAI,IAAI,SAAS,OAAO,EAAE,EAC7C,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,MAAM,KACT,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,CAS1B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,MAAM,KAAG,OAAO,CAAC,IAAI,CACI,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,GAAI,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAC5C,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,KACvB,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,CAUvB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,OAAO,GAAI,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAC/C,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,EACxB,QAAO,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,MAA0C,KACnE,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,CAUvB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,KAAK,GAAU,CAAC,EAC3B,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACxB,OAAM,YAAiB,KACtB,OAAO,CAAC,CAAC,CAeX,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAC3B,SAAS,OAAO,CAAC,CAAC,CAAC,EACnB,IAAI,MAAM,EACV,qBAAoC,KACnC,OAAO,CAAC,CAAC,CAWX,CAAC"}
|
package/dist/fn/index.js
CHANGED
|
@@ -17,7 +17,57 @@ var throttle = (fn, ms) => {
|
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
19
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
var once = (fn) => {
|
|
21
|
+
let called = false;
|
|
22
|
+
let result;
|
|
23
|
+
return (...args) => {
|
|
24
|
+
if (!called) {
|
|
25
|
+
called = true;
|
|
26
|
+
result = fn(...args);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
var memoize = (fn, keyFn = (...args) => JSON.stringify(args)) => {
|
|
32
|
+
const cache = /* @__PURE__ */ new Map();
|
|
33
|
+
return (...args) => {
|
|
34
|
+
const key = keyFn(...args);
|
|
35
|
+
const cached = cache.get(key);
|
|
36
|
+
if (cached !== void 0 || cache.has(key)) return cached;
|
|
37
|
+
const result = fn(...args);
|
|
38
|
+
cache.set(key, result);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
var retry = async (fn, opts = {}) => {
|
|
43
|
+
const { retries = 3, delay = 100, backoff = 2, onError } = opts;
|
|
44
|
+
let lastError;
|
|
45
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
lastError = err;
|
|
50
|
+
onError?.(err, attempt);
|
|
51
|
+
if (attempt < retries) {
|
|
52
|
+
await sleep(delay * Math.pow(backoff, attempt));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastError;
|
|
57
|
+
};
|
|
58
|
+
var withTimeout = (promise, ms, errorMessage = "Operation timed out") => {
|
|
59
|
+
let timeoutId;
|
|
60
|
+
const timeout = new Promise((_, reject) => {
|
|
61
|
+
timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);
|
|
62
|
+
});
|
|
63
|
+
return Promise.race([
|
|
64
|
+
promise.finally(() => {
|
|
65
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
66
|
+
}),
|
|
67
|
+
timeout
|
|
68
|
+
]);
|
|
69
|
+
};
|
|
20
70
|
|
|
21
|
-
export { debounce, sleep, throttle };
|
|
71
|
+
export { debounce, memoize, once, retry, sleep, throttle, withTimeout };
|
|
22
72
|
//# sourceMappingURL=index.js.map
|
|
23
73
|
//# sourceMappingURL=index.js.map
|
package/dist/fn/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fn/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/fn/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * 함수 호출을 지연시켜, 마지막 호출 후 `ms` 밀리초가 지나야 실제로 실행되도록 합니다.\n * 검색 입력, 윈도우 리사이즈 등 \"연속 입력이 멈춘 뒤\"에 한 번만 실행하고 싶을 때 사용합니다.\n *\n * @param fn - 디바운스할 함수\n * @param ms - 대기 시간 (밀리초)\n * @returns 디바운스가 적용된 새 함수\n * @example\n * ```ts\n * const onResize = debounce(() => console.log(\"resized\"), 200);\n * window.addEventListener(\"resize\", onResize);\n * // 사용자가 리사이즈를 멈춘 뒤 200ms 후에 한 번만 출력\n * ```\n */\nexport const debounce = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return (...args: Args) => {\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(() => fn(...args), ms);\n };\n};\n\n/**\n * `ms` 밀리초마다 최대 한 번씩만 함수가 실행되도록 제한합니다.\n * 스크롤/마우스 무브 등 너무 자주 호출되는 이벤트의 빈도를 줄일 때 사용합니다.\n *\n * @param fn - 스로틀할 함수\n * @param ms - 최소 호출 간격 (밀리초)\n * @returns 스로틀이 적용된 새 함수\n * @example\n * ```ts\n * const onScroll = throttle(() => console.log(\"scroll\"), 100);\n * window.addEventListener(\"scroll\", onScroll);\n * // 100ms에 최대 1번만 출력\n * ```\n */\nexport const throttle = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let last = 0;\n return (...args: Args) => {\n const now = Date.now();\n if (now - last >= ms) {\n last = now;\n fn(...args);\n }\n };\n};\n\n/**\n * 지정한 시간만큼 대기하는 Promise를 반환합니다.\n *\n * @param ms - 대기 시간 (밀리초)\n * @returns `ms` 이후 resolve되는 Promise\n * @example\n * ```ts\n * async function task() {\n * console.log(\"start\");\n * await sleep(1000);\n * console.log(\"1초 뒤\");\n * }\n * ```\n */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * 첫 호출의 결과를 저장하고, 이후 호출 시 인자에 관계없이 같은 결과를 반환합니다.\n * 초기화 함수나 싱글톤 생성 등 \"한 번만 실행되어야 하는 작업\"에 유용합니다.\n *\n * @param fn - 한 번만 실행할 함수\n * @returns 첫 호출 결과를 캐싱하는 래퍼 함수\n * @example\n * ```ts\n * const init = once(() => {\n * console.log(\"initialized\");\n * return { ready: true };\n * });\n *\n * init(); // \"initialized\" 출력 후 { ready: true } 반환\n * init(); // 출력 없음, 같은 객체 반환\n * ```\n */\nexport const once = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n): ((...args: Args) => R) => {\n let called = false;\n let result: R;\n return (...args: Args): R => {\n if (!called) {\n called = true;\n result = fn(...args);\n }\n return result;\n };\n};\n\n/**\n * 함수의 결과를 인자별로 캐싱합니다. 동일한 인자로 다시 호출되면 캐시된 결과를 반환합니다.\n *\n * 기본 캐시 키는 `JSON.stringify(args)`로 생성되므로 객체 인자도 값 비교가 가능합니다.\n * 필요하면 `keyFn`을 직접 지정해 키 생성 로직을 커스터마이즈하세요.\n *\n * @param fn - 결과를 캐싱할 함수\n * @param keyFn - 인자에서 캐시 키를 만드는 함수 (기본값: `JSON.stringify(args)`)\n * @returns 결과 캐싱이 적용된 래퍼 함수\n * @example\n * ```ts\n * const slow = memoize((n: number) => {\n * console.log(\"compute\", n);\n * return n * 2;\n * });\n *\n * slow(2); // \"compute 2\" 출력 후 4\n * slow(2); // 출력 없음, 4\n * slow(3); // \"compute 3\" 출력 후 6\n *\n * // 커스텀 키: 객체 id 기준 캐싱\n * const byId = memoize(\n * (user: { id: number; name: string }) => fetchProfile(user.id),\n * (user) => String(user.id),\n * );\n * ```\n */\nexport const memoize = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n keyFn: (...args: Args) => string = (...args) => JSON.stringify(args),\n): ((...args: Args) => R) => {\n const cache = new Map<string, R>();\n return (...args: Args): R => {\n const key = keyFn(...args);\n const cached = cache.get(key);\n if (cached !== undefined || cache.has(key)) return cached as R;\n const result = fn(...args);\n cache.set(key, result);\n return result;\n };\n};\n\n/**\n * {@link retry} 함수의 옵션.\n */\nexport interface RetryOptions {\n /** 추가 재시도 횟수. 총 시도 횟수는 `retries + 1`. 기본값 `3`. */\n retries?: number;\n /** 첫 재시도 전 대기 시간(ms). 기본값 `100`. */\n delay?: number;\n /** 매 재시도마다 대기 시간에 곱해질 배수 (지수 백오프). 기본값 `2`. */\n backoff?: number;\n /** 매 실패마다 호출되는 콜백. `attempt`는 0부터 시작하는 시도 인덱스. */\n onError?: (error: unknown, attempt: number) => void;\n}\n\n/**\n * 함수를 호출하다가 예외가 나면 지수 백오프로 재시도합니다. 동기/비동기 함수 모두 지원합니다.\n *\n * 모든 시도가 실패하면 마지막 에러를 throw합니다.\n *\n * @param fn - 실행할 함수 (동기 또는 비동기)\n * @param opts - 재시도 옵션 ({@link RetryOptions} 참고)\n * @returns 함수의 결과를 담은 Promise\n * @example\n * ```ts\n * // 기본 옵션: 최대 3회 재시도, 100ms부터 2배씩 백오프 (100, 200, 400ms)\n * const data = await retry(() => fetch(\"/api/data\").then((r) => r.json()));\n *\n * // 커스텀 옵션\n * await retry(unstableTask, {\n * retries: 5,\n * delay: 200,\n * backoff: 1.5,\n * onError: (err, attempt) => console.warn(`시도 ${attempt + 1} 실패`, err),\n * });\n * ```\n */\nexport const retry = async <T>(\n fn: () => Promise<T> | T,\n opts: RetryOptions = {},\n): Promise<T> => {\n const { retries = 3, delay = 100, backoff = 2, onError } = opts;\n let lastError: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n onError?.(err, attempt);\n if (attempt < retries) {\n await sleep(delay * Math.pow(backoff, attempt));\n }\n }\n }\n throw lastError;\n};\n\n/**\n * Promise에 타임아웃을 부여합니다. 지정한 시간 안에 settle되지 않으면 reject됩니다.\n *\n * 원본 Promise가 먼저 끝나면 내부 타이머는 자동으로 해제되므로 타이머 누수가 없습니다.\n *\n * @param promise - 타임아웃을 걸 Promise\n * @param ms - 제한 시간 (밀리초)\n * @param errorMessage - 타임아웃 시 던질 에러 메시지 (기본값 `\"Operation timed out\"`)\n * @returns 원본 결과 또는 타임아웃 에러로 settle되는 Promise\n * @example\n * ```ts\n * try {\n * const data = await withTimeout(fetch(\"/slow-api\"), 3000);\n * } catch (err) {\n * // 3초 안에 안 오면 여기로 떨어짐\n * }\n *\n * await withTimeout(longTask(), 5000, \"작업이 너무 오래 걸립니다\");\n * ```\n */\nexport const withTimeout = <T>(\n promise: Promise<T>,\n ms: number,\n errorMessage = \"Operation timed out\",\n): Promise<T> => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);\n });\n return Promise.race([\n promise.finally(() => {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }),\n timeout,\n ]);\n};\n"]}
|