@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
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { DateTime } from "../types/date-time";
|
|
2
|
-
import { DateOnly } from "../types/date-only";
|
|
3
|
-
import { Time } from "../types/time";
|
|
4
|
-
import { Uuid } from "../types/uuid";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Worker 간 전송 가능한 객체 타입
|
|
8
|
-
*
|
|
9
|
-
* 이 코드에서는 ArrayBuffer만 사용됩니다.
|
|
10
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
|
|
11
|
-
*/
|
|
12
|
-
type Transferable = ArrayBuffer;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Transferable 변환 유틸리티 함수
|
|
16
|
-
*
|
|
17
|
-
* Worker 간 데이터 전송을 위한 직렬화/역직렬화를 수행합니다.
|
|
18
|
-
* structuredClone이 지원하지 않는 커스텀 타입들을 처리합니다.
|
|
19
|
-
*
|
|
20
|
-
* 지원 타입:
|
|
21
|
-
* - Date, DateTime, DateOnly, Time, Uuid, RegExp
|
|
22
|
-
* - Error (cause, code, detail 포함)
|
|
23
|
-
* - Uint8Array (다른 TypedArray는 미지원, 일반 객체로 처리됨)
|
|
24
|
-
* - Array, Map, Set, 일반 객체
|
|
25
|
-
*
|
|
26
|
-
* @note 순환 참조가 있으면 transferableEncode 시 TypeError 발생 (경로 정보 포함)
|
|
27
|
-
* @note 동일 객체가 여러 곳에서 참조되면 캐시된 인코딩 결과를 재사용합니다
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // Worker로 데이터 전송
|
|
31
|
-
* const { result, transferList } = transferableEncode(data);
|
|
32
|
-
* worker.postMessage(result, transferList);
|
|
33
|
-
*
|
|
34
|
-
* // Worker에서 데이터 수신
|
|
35
|
-
* const decoded = transferableDecode(event.data);
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
//#region encode
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 심플리즘 타입을 사용한 객체를 일반 객체로 변환
|
|
42
|
-
* Worker에 전송할 수 있는 형태로 직렬화
|
|
43
|
-
*
|
|
44
|
-
* @throws 순환 참조 감지 시 TypeError
|
|
45
|
-
*/
|
|
46
|
-
export function transferableEncode(obj: unknown): {
|
|
47
|
-
result: unknown;
|
|
48
|
-
transferList: Transferable[];
|
|
49
|
-
} {
|
|
50
|
-
const transferList: Transferable[] = [];
|
|
51
|
-
const ancestors = new Set<object>();
|
|
52
|
-
const cache = new Map<object, unknown>();
|
|
53
|
-
const result = encodeImpl(obj, transferList, [], ancestors, cache);
|
|
54
|
-
return { result, transferList };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function encodeImpl(
|
|
58
|
-
obj: unknown,
|
|
59
|
-
transferList: Transferable[],
|
|
60
|
-
path: (string | number)[],
|
|
61
|
-
ancestors: Set<object>,
|
|
62
|
-
cache: Map<object, unknown>,
|
|
63
|
-
): unknown {
|
|
64
|
-
if (obj == null) return obj;
|
|
65
|
-
|
|
66
|
-
// 객체 타입 처리: 순환 감지 + 캐시
|
|
67
|
-
if (typeof obj === "object") {
|
|
68
|
-
// 순환 참조 감지 (현재 재귀 스택에 있는 객체)
|
|
69
|
-
if (ancestors.has(obj)) {
|
|
70
|
-
const currentPath = path.length > 0 ? path.join(".") : "root";
|
|
71
|
-
throw new TypeError(`순환 참조가 감지되었습니다: ${currentPath}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 캐시 히트 → 이전 인코딩 결과 재사용
|
|
75
|
-
const cached = cache.get(obj);
|
|
76
|
-
if (cached !== undefined) return cached;
|
|
77
|
-
|
|
78
|
-
// 재귀 스택에 추가
|
|
79
|
-
ancestors.add(obj);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let result: unknown;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// 1. Uint8Array
|
|
86
|
-
if (obj instanceof Uint8Array) {
|
|
87
|
-
// SharedArrayBuffer는 이미 공유 메모리이므로 transferList에 추가하지 않음
|
|
88
|
-
// ArrayBuffer만 transferList에 추가
|
|
89
|
-
const isSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined" && obj.buffer instanceof SharedArrayBuffer;
|
|
90
|
-
const buffer = obj.buffer as ArrayBuffer;
|
|
91
|
-
if (!isSharedArrayBuffer && !transferList.includes(buffer)) {
|
|
92
|
-
transferList.push(buffer);
|
|
93
|
-
}
|
|
94
|
-
result = obj;
|
|
95
|
-
}
|
|
96
|
-
// 2. 특수 타입 변환 (JSON.stringify 없이 구조체로 변환)
|
|
97
|
-
else if (obj instanceof Date) {
|
|
98
|
-
result = { __type__: "Date", data: obj.getTime() };
|
|
99
|
-
} else if (obj instanceof DateTime) {
|
|
100
|
-
result = { __type__: "DateTime", data: obj.tick };
|
|
101
|
-
} else if (obj instanceof DateOnly) {
|
|
102
|
-
result = { __type__: "DateOnly", data: obj.tick };
|
|
103
|
-
} else if (obj instanceof Time) {
|
|
104
|
-
result = { __type__: "Time", data: obj.tick };
|
|
105
|
-
} else if (obj instanceof Uuid) {
|
|
106
|
-
result = { __type__: "Uuid", data: obj.toString() };
|
|
107
|
-
} else if (obj instanceof RegExp) {
|
|
108
|
-
result = { __type__: "RegExp", data: { source: obj.source, flags: obj.flags } };
|
|
109
|
-
} else if (obj instanceof Error) {
|
|
110
|
-
const errObj = obj as Error & {
|
|
111
|
-
code?: unknown;
|
|
112
|
-
detail?: unknown;
|
|
113
|
-
};
|
|
114
|
-
result = {
|
|
115
|
-
__type__: "Error",
|
|
116
|
-
data: {
|
|
117
|
-
name: errObj.name,
|
|
118
|
-
message: errObj.message,
|
|
119
|
-
stack: errObj.stack,
|
|
120
|
-
...(errObj.code !== undefined ? { code: errObj.code } : {}),
|
|
121
|
-
...(errObj.detail !== undefined
|
|
122
|
-
? { detail: encodeImpl(errObj.detail, transferList, [...path, "detail"], ancestors, cache) }
|
|
123
|
-
: {}),
|
|
124
|
-
...(errObj.cause !== undefined
|
|
125
|
-
? { cause: encodeImpl(errObj.cause, transferList, [...path, "cause"], ancestors, cache) }
|
|
126
|
-
: {}),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// 3. 배열 재귀 순회
|
|
131
|
-
else if (Array.isArray(obj)) {
|
|
132
|
-
result = obj.map((item, idx) => encodeImpl(item, transferList, [...path, idx], ancestors, cache));
|
|
133
|
-
}
|
|
134
|
-
// 4. Map 재귀 순회
|
|
135
|
-
else if (obj instanceof Map) {
|
|
136
|
-
let idx = 0;
|
|
137
|
-
result = new Map(
|
|
138
|
-
Array.from(obj.entries()).map(([k, v]) => {
|
|
139
|
-
const keyPath = [...path, `Map[${idx}].key`];
|
|
140
|
-
const valuePath = [...path, `Map[${idx}].value`];
|
|
141
|
-
idx++;
|
|
142
|
-
return [
|
|
143
|
-
encodeImpl(k, transferList, keyPath, ancestors, cache),
|
|
144
|
-
encodeImpl(v, transferList, valuePath, ancestors, cache),
|
|
145
|
-
];
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
// 5. Set 재귀 순회
|
|
150
|
-
else if (obj instanceof Set) {
|
|
151
|
-
let idx = 0;
|
|
152
|
-
result = new Set(
|
|
153
|
-
Array.from(obj).map((v) => encodeImpl(v, transferList, [...path, `Set[${idx++}]`], ancestors, cache)),
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
// 6. 일반 객체 재귀 순회
|
|
157
|
-
else if (typeof obj === "object") {
|
|
158
|
-
const res: Record<string, unknown> = {};
|
|
159
|
-
const record = obj as Record<string, unknown>;
|
|
160
|
-
for (const key of Object.keys(record)) {
|
|
161
|
-
res[key] = encodeImpl(record[key], transferList, [...path, key], ancestors, cache);
|
|
162
|
-
}
|
|
163
|
-
result = res;
|
|
164
|
-
}
|
|
165
|
-
// 7. 원시 타입
|
|
166
|
-
else {
|
|
167
|
-
return obj;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 캐시 저장 (성공 시에만)
|
|
171
|
-
if (typeof obj === "object") {
|
|
172
|
-
cache.set(obj, result);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return result;
|
|
176
|
-
} finally {
|
|
177
|
-
// 재귀 스택에서 제거 (예외 시에도 반드시 실행)
|
|
178
|
-
if (typeof obj === "object") {
|
|
179
|
-
ancestors.delete(obj);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
//#endregion
|
|
185
|
-
|
|
186
|
-
//#region decode
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* serialize 객체를 심플리즘 타입 사용 객체로 변환
|
|
190
|
-
* Worker로부터 받은 데이터를 역직렬화
|
|
191
|
-
*/
|
|
192
|
-
export function transferableDecode(obj: unknown): unknown {
|
|
193
|
-
if (obj == null) return obj;
|
|
194
|
-
|
|
195
|
-
// 1. 특수 타입 복원
|
|
196
|
-
if (typeof obj === "object" && "__type__" in obj && "data" in obj) {
|
|
197
|
-
const typed = obj as { __type__: string; data: unknown };
|
|
198
|
-
const data = typed.data;
|
|
199
|
-
|
|
200
|
-
if (typed.__type__ === "Date" && typeof data === "number") return new Date(data);
|
|
201
|
-
if (typed.__type__ === "DateTime" && typeof data === "number") return new DateTime(data);
|
|
202
|
-
if (typed.__type__ === "DateOnly" && typeof data === "number") return new DateOnly(data);
|
|
203
|
-
if (typed.__type__ === "Time" && typeof data === "number") return new Time(data);
|
|
204
|
-
if (typed.__type__ === "Uuid" && typeof data === "string") return new Uuid(data);
|
|
205
|
-
if (typed.__type__ === "RegExp" && typeof data === "object" && data !== null) {
|
|
206
|
-
const regexData = data as { source: string; flags: string };
|
|
207
|
-
return new RegExp(regexData.source, regexData.flags);
|
|
208
|
-
}
|
|
209
|
-
if (typed.__type__ === "Error" && typeof data === "object" && data !== null) {
|
|
210
|
-
const errorData = data as {
|
|
211
|
-
name: string;
|
|
212
|
-
message: string;
|
|
213
|
-
stack?: string;
|
|
214
|
-
code?: unknown;
|
|
215
|
-
cause?: unknown;
|
|
216
|
-
detail?: unknown;
|
|
217
|
-
};
|
|
218
|
-
const err = new Error(errorData.message) as Error & {
|
|
219
|
-
code?: unknown;
|
|
220
|
-
detail?: unknown;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
err.name = errorData.name;
|
|
224
|
-
err.stack = errorData.stack;
|
|
225
|
-
|
|
226
|
-
if (errorData.code !== undefined) err.code = errorData.code;
|
|
227
|
-
if (errorData.cause !== undefined) (err as Error).cause = transferableDecode(errorData.cause);
|
|
228
|
-
if (errorData.detail !== undefined) err.detail = transferableDecode(errorData.detail);
|
|
229
|
-
return err;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 2. 배열 재귀 (새 배열 생성)
|
|
234
|
-
if (Array.isArray(obj)) {
|
|
235
|
-
return obj.map((item) => transferableDecode(item));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 3. Map 재귀
|
|
239
|
-
if (obj instanceof Map) {
|
|
240
|
-
const newMap = new Map<unknown, unknown>();
|
|
241
|
-
for (const [k, v] of obj) {
|
|
242
|
-
newMap.set(transferableDecode(k), transferableDecode(v));
|
|
243
|
-
}
|
|
244
|
-
return newMap;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 4. Set 재귀
|
|
248
|
-
if (obj instanceof Set) {
|
|
249
|
-
const newSet = new Set<unknown>();
|
|
250
|
-
for (const v of obj) {
|
|
251
|
-
newSet.add(transferableDecode(v));
|
|
252
|
-
}
|
|
253
|
-
return newSet;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 5. 객체 재귀 (새 객체 생성)
|
|
257
|
-
if (typeof obj === "object") {
|
|
258
|
-
const record = obj as Record<string, unknown>;
|
|
259
|
-
const result: Record<string, unknown> = {};
|
|
260
|
-
for (const key of Object.keys(record)) {
|
|
261
|
-
result[key] = transferableDecode(record[key]);
|
|
262
|
-
}
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return obj;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
//#endregion
|
package/src/utils/wait.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 대기 유틸리티 함수
|
|
3
|
-
*/
|
|
4
|
-
import { TimeoutError } from "../errors/timeout-error";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 조건이 참이 될 때까지 대기
|
|
8
|
-
* @param forwarder 조건 함수
|
|
9
|
-
* @param milliseconds 체크 간격 (기본: 100ms)
|
|
10
|
-
* @param maxCount 최대 시도 횟수 (undefined면 무제한)
|
|
11
|
-
*
|
|
12
|
-
* @note 조건이 첫 번째 호출에서 true면 즉시 반환됩니다.
|
|
13
|
-
* @example
|
|
14
|
-
* // maxCount=3: 최대 3번 조건 확인 후 모두 false면 TimeoutError
|
|
15
|
-
* await waitUntil(() => someCondition, 100, 3);
|
|
16
|
-
* @throws TimeoutError 최대 시도 횟수 초과 시
|
|
17
|
-
*/
|
|
18
|
-
export async function waitUntil(
|
|
19
|
-
forwarder: () => boolean | Promise<boolean>,
|
|
20
|
-
milliseconds?: number,
|
|
21
|
-
maxCount?: number,
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
let count = 0;
|
|
24
|
-
while (!(await forwarder())) {
|
|
25
|
-
count++;
|
|
26
|
-
if (maxCount !== undefined && count >= maxCount) {
|
|
27
|
-
throw new TimeoutError(count);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
await waitTime(milliseconds ?? 100);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 지정된 시간만큼 대기
|
|
36
|
-
* @param millisecond 대기 시간 (ms)
|
|
37
|
-
*/
|
|
38
|
-
export async function waitTime(millisecond: number): Promise<void> {
|
|
39
|
-
return new Promise<void>((resolve) => setTimeout(resolve, millisecond));
|
|
40
|
-
}
|
package/src/utils/xml.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XML 변환 유틸리티
|
|
3
|
-
*/
|
|
4
|
-
import type { XmlBuilderOptions } from "fast-xml-parser";
|
|
5
|
-
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
6
|
-
|
|
7
|
-
//#region parse
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* XML 문자열을 객체로 파싱
|
|
11
|
-
* @param str XML 문자열
|
|
12
|
-
* @param options 옵션
|
|
13
|
-
* @param options.stripTagPrefix 태그 prefix 제거 여부 (namespace)
|
|
14
|
-
* @returns 파싱된 객체. 구조:
|
|
15
|
-
* - 속성: `$` 객체에 그룹화
|
|
16
|
-
* - 텍스트 노드: `_` 키에 저장
|
|
17
|
-
* - 자식 요소: 배열로 변환 (루트 요소 제외)
|
|
18
|
-
* @example
|
|
19
|
-
* xmlParse('<root id="1"><item>hello</item></root>');
|
|
20
|
-
* // { root: { $: { id: "1" }, item: [{ _: "hello" }] } }
|
|
21
|
-
*/
|
|
22
|
-
export function xmlParse(str: string, options?: { stripTagPrefix?: boolean }): unknown {
|
|
23
|
-
const result = new XMLParser({
|
|
24
|
-
ignoreAttributes: false,
|
|
25
|
-
attributeNamePrefix: "",
|
|
26
|
-
attributesGroupName: "$",
|
|
27
|
-
parseAttributeValue: false,
|
|
28
|
-
parseTagValue: false,
|
|
29
|
-
textNodeName: "_",
|
|
30
|
-
isArray: (_tagName: string, jPath: string, _isLeafNode: boolean, isAttribute: boolean) => {
|
|
31
|
-
return !isAttribute && jPath.split(".").length > 1;
|
|
32
|
-
},
|
|
33
|
-
}).parse(str) as unknown;
|
|
34
|
-
return options?.stripTagPrefix ? stripTagPrefix(result) : result;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
//#endregion
|
|
38
|
-
|
|
39
|
-
//#region stringify
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 객체를 XML 문자열로 직렬화
|
|
43
|
-
* @param obj 직렬화할 객체
|
|
44
|
-
* @param options fast-xml-parser XmlBuilderOptions (선택)
|
|
45
|
-
* @returns XML 문자열
|
|
46
|
-
* @example
|
|
47
|
-
* xmlStringify({
|
|
48
|
-
* root: {
|
|
49
|
-
* $: { id: "1" },
|
|
50
|
-
* item: [{ _: "hello" }, { _: "world" }],
|
|
51
|
-
* },
|
|
52
|
-
* });
|
|
53
|
-
* // '<root id="1"><item>hello</item><item>world</item></root>'
|
|
54
|
-
*/
|
|
55
|
-
export function xmlStringify(obj: unknown, options?: XmlBuilderOptions): string {
|
|
56
|
-
return new XMLBuilder({
|
|
57
|
-
ignoreAttributes: false,
|
|
58
|
-
attributeNamePrefix: "",
|
|
59
|
-
attributesGroupName: "$",
|
|
60
|
-
suppressBooleanAttributes: false,
|
|
61
|
-
textNodeName: "_",
|
|
62
|
-
...options,
|
|
63
|
-
}).build(obj);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
|
|
68
|
-
//#region private
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 태그 이름에서 namespace prefix 제거
|
|
72
|
-
* @note XML 파싱 결과에서 "ns:tag" 형태의 namespace prefix를 제거하여 태그 이름만 남긴다.
|
|
73
|
-
* 이를 통해 namespace를 고려하지 않고 일관된 방식으로 XML 데이터에 접근할 수 있다.
|
|
74
|
-
* 단, 속성(attribute)은 prefix를 유지한다.
|
|
75
|
-
*/
|
|
76
|
-
function stripTagPrefix(obj: unknown): unknown {
|
|
77
|
-
if (Array.isArray(obj)) {
|
|
78
|
-
return obj.map((item) => stripTagPrefix(item));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (typeof obj === "object" && obj !== null) {
|
|
82
|
-
const newObj: Record<string, unknown> = {};
|
|
83
|
-
const record = obj as Record<string, unknown>;
|
|
84
|
-
|
|
85
|
-
for (const key of Object.keys(record)) {
|
|
86
|
-
const value = record[key];
|
|
87
|
-
|
|
88
|
-
// Attribute는 prefix를 제거하면 안 된다.
|
|
89
|
-
if (key === "$") {
|
|
90
|
-
newObj[key] = value;
|
|
91
|
-
} else {
|
|
92
|
-
// 태그 이름에서만 첫 번째 ":"을 기준으로 prefix 제거
|
|
93
|
-
const colonIndex = key.indexOf(":");
|
|
94
|
-
const cleanKey = colonIndex !== -1 ? key.slice(colonIndex + 1) : key;
|
|
95
|
-
newObj[cleanKey] = stripTagPrefix(value);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return newObj;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return obj;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
//#endregion
|
package/src/zip/sd-zip.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ZIP 파일 처리 유틸리티
|
|
3
|
-
*/
|
|
4
|
-
import type { FileEntry } from "@zip.js/zip.js";
|
|
5
|
-
import { BlobReader, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } from "@zip.js/zip.js";
|
|
6
|
-
import type { Bytes } from "../common.types";
|
|
7
|
-
|
|
8
|
-
export interface ZipArchiveProgress {
|
|
9
|
-
fileName: string;
|
|
10
|
-
totalSize: number;
|
|
11
|
-
extractedSize: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* ZIP 아카이브 처리 클래스
|
|
16
|
-
*
|
|
17
|
-
* ZIP 파일의 읽기, 쓰기, 압축/해제를 처리합니다.
|
|
18
|
-
* 내부 캐시를 사용하여 동일 파일의 중복 압축 해제를 방지합니다.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // ZIP 파일 읽기
|
|
22
|
-
* await using archive = new ZipArchive(zipBytes);
|
|
23
|
-
* const content = await archive.get("file.txt");
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* // ZIP 파일 생성
|
|
27
|
-
* await using archive = new ZipArchive();
|
|
28
|
-
* archive.write("file.txt", textBytes);
|
|
29
|
-
* archive.write("data.json", jsonBytes);
|
|
30
|
-
* const zipBytes = await archive.compress();
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* // 전체 압축 해제 (진행률 표시)
|
|
34
|
-
* await using archive = new ZipArchive(zipBytes);
|
|
35
|
-
* const files = await archive.extractAll((progress) => {
|
|
36
|
-
* console.log(`${progress.fileName}: ${progress.extractedSize}/${progress.totalSize}`);
|
|
37
|
-
* });
|
|
38
|
-
*/
|
|
39
|
-
export class ZipArchive {
|
|
40
|
-
private readonly _reader?: ZipReader<Blob | Bytes>;
|
|
41
|
-
private readonly _cache = new Map<string, Bytes | undefined>();
|
|
42
|
-
private _entries?: Awaited<ReturnType<ZipReader<Blob | Bytes>["getEntries"]>>;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* ZipArchive 생성
|
|
46
|
-
* @param data ZIP 데이터 (생략 시 새 아카이브 생성)
|
|
47
|
-
*/
|
|
48
|
-
constructor(data?: Blob | Bytes) {
|
|
49
|
-
if (!data) return;
|
|
50
|
-
|
|
51
|
-
if (data instanceof Uint8Array) {
|
|
52
|
-
this._reader = new ZipReader(new Uint8ArrayReader(data));
|
|
53
|
-
} else {
|
|
54
|
-
this._reader = new ZipReader(new BlobReader(data));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private async _getEntries() {
|
|
59
|
-
if (this._entries == null && this._reader != null) {
|
|
60
|
-
this._entries = await this._reader.getEntries();
|
|
61
|
-
}
|
|
62
|
-
return this._entries;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
//#region extractAll
|
|
66
|
-
/**
|
|
67
|
-
* 모든 파일을 압축 해제
|
|
68
|
-
* @param progressCallback 진행률 콜백
|
|
69
|
-
*/
|
|
70
|
-
async extractAll(progressCallback?: (progress: ZipArchiveProgress) => void): Promise<Map<string, Bytes | undefined>> {
|
|
71
|
-
const entries = await this._getEntries();
|
|
72
|
-
if (entries == null) return this._cache;
|
|
73
|
-
|
|
74
|
-
// 압축 해제 대상 크기 총합 계산
|
|
75
|
-
const totalSize = entries.filter((e) => !e.directory).reduce((acc, e) => acc + e.uncompressedSize, 0);
|
|
76
|
-
|
|
77
|
-
let totalExtracted = 0;
|
|
78
|
-
for (const entry of entries) {
|
|
79
|
-
if (entry.directory) continue;
|
|
80
|
-
|
|
81
|
-
progressCallback?.({
|
|
82
|
-
fileName: entry.filename,
|
|
83
|
-
totalSize: totalSize,
|
|
84
|
-
extractedSize: totalExtracted,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const entryBytes =
|
|
88
|
-
this._cache.get(entry.filename) ??
|
|
89
|
-
(await entry.getData(new Uint8ArrayWriter(), {
|
|
90
|
-
onprogress: (extracted) => {
|
|
91
|
-
const currentTotal = totalExtracted + extracted;
|
|
92
|
-
|
|
93
|
-
progressCallback?.({
|
|
94
|
-
fileName: entry.filename,
|
|
95
|
-
totalSize: totalSize,
|
|
96
|
-
extractedSize: currentTotal,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return undefined;
|
|
100
|
-
},
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
this._cache.set(entry.filename, entryBytes);
|
|
104
|
-
|
|
105
|
-
// 개별 파일이 끝나면 누적 처리
|
|
106
|
-
totalExtracted += entry.uncompressedSize;
|
|
107
|
-
|
|
108
|
-
progressCallback?.({
|
|
109
|
-
fileName: entry.filename,
|
|
110
|
-
totalSize: totalSize,
|
|
111
|
-
extractedSize: totalExtracted,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return this._cache;
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
|
|
119
|
-
//#region get
|
|
120
|
-
/**
|
|
121
|
-
* 특정 파일 압축 해제
|
|
122
|
-
* @param fileName 파일 이름
|
|
123
|
-
*/
|
|
124
|
-
async get(fileName: string): Promise<Bytes | undefined> {
|
|
125
|
-
if (this._cache.has(fileName)) {
|
|
126
|
-
return this._cache.get(fileName);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const entries = await this._getEntries();
|
|
130
|
-
if (entries == null) {
|
|
131
|
-
this._cache.set(fileName, undefined);
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const entry = entries.find((item) => item.filename === fileName) as FileEntry | undefined;
|
|
136
|
-
if (!entry) {
|
|
137
|
-
this._cache.set(fileName, undefined);
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const bytes = await entry.getData(new Uint8ArrayWriter());
|
|
142
|
-
this._cache.set(fileName, bytes);
|
|
143
|
-
return bytes;
|
|
144
|
-
}
|
|
145
|
-
//#endregion
|
|
146
|
-
|
|
147
|
-
//#region exists
|
|
148
|
-
/**
|
|
149
|
-
* 파일 존재 여부 확인
|
|
150
|
-
* @param fileName 파일 이름
|
|
151
|
-
*/
|
|
152
|
-
async exists(fileName: string): Promise<boolean> {
|
|
153
|
-
if (this._cache.has(fileName)) {
|
|
154
|
-
return this._cache.get(fileName) != null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const entries = await this._getEntries();
|
|
158
|
-
if (entries == null) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const entry = entries.find((item) => item.filename === fileName) as FileEntry | undefined;
|
|
163
|
-
return entry !== undefined;
|
|
164
|
-
}
|
|
165
|
-
//#endregion
|
|
166
|
-
|
|
167
|
-
//#region write
|
|
168
|
-
/**
|
|
169
|
-
* 파일 쓰기 (캐시에 저장)
|
|
170
|
-
* @param fileName 파일 이름
|
|
171
|
-
* @param bytes 파일 내용
|
|
172
|
-
*/
|
|
173
|
-
write(fileName: string, bytes: Bytes): void {
|
|
174
|
-
this._cache.set(fileName, bytes);
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
|
|
178
|
-
//#region compress
|
|
179
|
-
/**
|
|
180
|
-
* 캐시된 파일들을 ZIP으로 압축
|
|
181
|
-
*
|
|
182
|
-
* @remarks
|
|
183
|
-
* 내부적으로 `extractAll()`을 호출하여 모든 파일을 메모리에 로드한 후 압축합니다.
|
|
184
|
-
* 대용량 ZIP 파일의 경우 메모리 사용량에 주의가 필요합니다.
|
|
185
|
-
*/
|
|
186
|
-
async compress(): Promise<Bytes> {
|
|
187
|
-
const fileMap = await this.extractAll();
|
|
188
|
-
|
|
189
|
-
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
190
|
-
|
|
191
|
-
for (const key of fileMap.keys()) {
|
|
192
|
-
const fileBytes = fileMap.get(key);
|
|
193
|
-
if (!fileBytes) continue;
|
|
194
|
-
|
|
195
|
-
await writer.add(key, new Uint8ArrayReader(fileBytes));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return writer.close();
|
|
199
|
-
}
|
|
200
|
-
//#endregion
|
|
201
|
-
|
|
202
|
-
//#region close
|
|
203
|
-
/**
|
|
204
|
-
* 리더 닫기 및 캐시 정리
|
|
205
|
-
*/
|
|
206
|
-
async close(): Promise<void> {
|
|
207
|
-
await this._reader?.close();
|
|
208
|
-
this._cache.clear();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* await using 지원
|
|
213
|
-
*/
|
|
214
|
-
async [Symbol.asyncDispose](): Promise<void> {
|
|
215
|
-
await this.close();
|
|
216
|
-
}
|
|
217
|
-
//#endregion
|
|
218
|
-
}
|