@simplysm/core-common 13.0.72 → 13.0.75
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 +104 -54
- package/package.json +1 -1
- package/src/utils/str.ts +260 -260
- package/src/utils/transferable.ts +284 -284
package/README.md
CHANGED
|
@@ -1,77 +1,127 @@
|
|
|
1
1
|
# @simplysm/core-common
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Common utility package providing environment variables, array extensions, error classes, date/time types, async features, and a wide range of utility functions. Works in both browser and Node.js environments.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
```bash
|
|
7
8
|
pnpm add @simplysm/core-common
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Table of Contents
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- [Array Extensions](#array-extensions)
|
|
14
|
+
- [Types](#types)
|
|
15
|
+
- [Utils](#utils)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Errors](#errors)
|
|
18
|
+
- [Zip](#zip)
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|--------|---------|-------------|------|
|
|
15
|
-
| `src/env.ts` | `env` | Runtime environment configuration object | - |
|
|
16
|
-
| `src/extensions/arr-ext.ts` | `ArrayDiffsResult`, `ArrayDiffs2Result`, `TreeArray`, `ComparableType` | Array diff, tree, and comparison utilities via prototype augmentation | `array-extension.spec.ts` |
|
|
20
|
+
---
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
## Array Extensions
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|--------|---------|-------------|------|
|
|
22
|
-
| `src/errors/sd-error.ts` | `SdError` | Base error class with name-based error chain support | `errors.spec.ts` |
|
|
23
|
-
| `src/errors/argument-error.ts` | `ArgumentError` | Error thrown when a function receives an invalid argument | `errors.spec.ts` |
|
|
24
|
-
| `src/errors/not-implemented-error.ts` | `NotImplementedError` | Error indicating an unimplemented method or feature | `errors.spec.ts` |
|
|
25
|
-
| `src/errors/timeout-error.ts` | `TimeoutError` | Error thrown when an operation exceeds its time limit | `errors.spec.ts` |
|
|
24
|
+
Prototype extensions added to `Array` and `ReadonlyArray` as a side effect of importing from this package. Covers querying, grouping, diffing, sorting, and async iteration.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
[Full documentation](docs/array-extensions.md)
|
|
28
27
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
28
|
+
| Symbol | Description |
|
|
29
|
+
|--------|-------------|
|
|
30
|
+
| [`single`](docs/array-extensions.md#readonly-array-methods) | Returns the sole matching element (throws on multiple matches) |
|
|
31
|
+
| [`first`](docs/array-extensions.md#readonly-array-methods) | First matching element |
|
|
32
|
+
| [`last`](docs/array-extensions.md#readonly-array-methods) | Last matching element |
|
|
33
|
+
| [`filterExists`](docs/array-extensions.md#readonly-array-methods) | Removes `null`/`undefined` entries |
|
|
34
|
+
| [`groupBy`](docs/array-extensions.md#readonly-array-methods) | Groups elements by key |
|
|
35
|
+
| [`toMap`](docs/array-extensions.md#readonly-array-methods) | Converts to `Map` |
|
|
36
|
+
| [`toTree`](docs/array-extensions.md#readonly-array-methods) | Builds a tree from a flat list |
|
|
37
|
+
| [`distinct`](docs/array-extensions.md#readonly-array-methods) | Returns unique elements |
|
|
38
|
+
| [`orderBy`](docs/array-extensions.md#readonly-array-methods) / [`orderByDesc`](docs/array-extensions.md#readonly-array-methods) | Sorted copy (ascending/descending) |
|
|
39
|
+
| [`diffs`](docs/array-extensions.md#readonly-array-methods) / [`oneWayDiffs`](docs/array-extensions.md#readonly-array-methods) | Array diff comparison |
|
|
40
|
+
| [`insert`](docs/array-extensions.md#mutable-array-methods) / [`remove`](docs/array-extensions.md#mutable-array-methods) / [`toggle`](docs/array-extensions.md#mutable-array-methods) | In-place mutation helpers |
|
|
41
|
+
| [`ArrayDiffsResult`](docs/array-extensions.md#related-types) / [`TreeArray`](docs/array-extensions.md#related-types) | Related TypeScript types |
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
---
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|--------|---------|-------------|------|
|
|
41
|
-
| `src/features/debounce-queue.ts` | `DebounceQueue` | Queue that debounces rapid calls into a single delayed execution | `debounce-queue.spec.ts` |
|
|
42
|
-
| `src/features/serial-queue.ts` | `SerialQueue` | Queue that serializes async operations to run one at a time | `serial-queue.spec.ts` |
|
|
43
|
-
| `src/features/event-emitter.ts` | `EventEmitter` | Type-safe event emitter with on/off/emit pattern | `sd-event-emitter.spec.ts` |
|
|
45
|
+
## Types
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
Immutable value types and utility type aliases. All support parsing, formatting, and arithmetic.
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|--------|---------|-------------|------|
|
|
49
|
-
| `src/utils/date-format.ts` | `DtNormalizedMonth`, `normalizeMonth`, `convert12To24`, `formatDate` | Date formatting and month normalization utilities | `date-format.spec.ts` |
|
|
50
|
-
| `src/utils/bytes.ts` | `bytesConcat`, `bytesToHex`, `bytesFromHex`, `bytesToBase64`, `bytesFromBase64` | Binary conversion utilities (hex, base64, concat) | `bytes-utils.spec.ts` |
|
|
51
|
-
| `src/utils/json.ts` | `jsonStringify`, `jsonParse` | JSON stringify/parse with custom type support (DateTime, DateOnly, etc.) | `json.spec.ts` |
|
|
52
|
-
| `src/utils/num.ts` | `numParseInt`, `numParseRoundedInt`, `numParseFloat`, `numIsNullOrEmpty`, `numFormat` | Number parsing and formatting utilities | `number.spec.ts` |
|
|
53
|
-
| `src/utils/obj.ts` | `objClone`, `EqualOptions`, `objEqual`, `ObjMergeOptions`, `objMerge`, `ObjMerge3KeyOptions`, `objMerge3`, `objOmit`, `objOmitByFilter`, `objPick`, `objGetChainValue`, `objGetChainValueByDepth`, `objSetChainValue`, `objDeleteChainValue`, `objClearUndefined`, `objClear`, `objNullToUndefined`, `objUnflatten`, `ObjUndefToOptional`, `ObjOptionalToUndef`, `objKeys`, `objEntries`, `objFromEntries`, `objMap` | Deep object utilities (clone, equal, merge, pick, omit, chain access) | `object.spec.ts` |
|
|
54
|
-
| `src/utils/primitive.ts` | `getPrimitiveTypeStr` | Primitive type string detection utility | `primitive.spec.ts` |
|
|
55
|
-
| `src/utils/str.ts` | `koreanGetSuffix`, `strReplaceFullWidth`, `strToPascalCase`, `strToCamelCase`, `strToKebabCase`, `strToSnakeCase`, `strIsNullOrEmpty`, `strInsert` | String utilities (case conversion, Korean suffix, full-width replacement) | `string.spec.ts` |
|
|
56
|
-
| `src/utils/template-strings.ts` | `js`, `ts`, `html`, `tsql`, `mysql`, `pgsql` | Tagged template literals for JS, TS, HTML, SQL syntax highlighting | `template-strings.spec.ts` |
|
|
57
|
-
| `src/utils/transferable.ts` | `transferableEncode`, `transferableDecode` | Encode/decode objects with Transferable types for structured clone | `transferable.spec.ts` |
|
|
58
|
-
| `src/utils/wait.ts` | `waitUntil`, `waitTime` | Async wait utilities (until condition, timed delay) | `wait.spec.ts` |
|
|
59
|
-
| `src/utils/xml.ts` | `xmlParse`, `xmlStringify` | XML parse and stringify utilities | `xml.spec.ts` |
|
|
60
|
-
| `src/utils/path.ts` | `pathJoin`, `pathBasename`, `pathExtname` | Platform-independent path join, basename, and extension utilities | `path.spec.ts` |
|
|
61
|
-
| `src/utils/error.ts` | `errorMessage` | Extract error message string from unknown error values | - |
|
|
49
|
+
[Full documentation](docs/types.md)
|
|
62
50
|
|
|
63
|
-
|
|
51
|
+
| Symbol | Description |
|
|
52
|
+
|--------|-------------|
|
|
53
|
+
| [`Uuid`](docs/types.md#uuid) | UUID v4 using `crypto.getRandomValues` |
|
|
54
|
+
| [`LazyGcMap`](docs/types.md#lazygcmaptkey-tvalue) | `Map` with automatic expiry-based garbage collection |
|
|
55
|
+
| [`DateTime`](docs/types.md#datetime) | Immutable date-time (millisecond precision, local timezone) |
|
|
56
|
+
| [`DateOnly`](docs/types.md#dateonly) | Immutable date without time; includes week-sequence helpers |
|
|
57
|
+
| [`Time`](docs/types.md#time) | Immutable time without date; 24-hour wrap normalization |
|
|
58
|
+
| [`Bytes`](docs/types.md#primitive-types) / [`PrimitiveType`](docs/types.md#primitive-types) | Primitive type aliases |
|
|
59
|
+
| [`DeepPartial`](docs/types.md#utility-types) / [`ObjUndefToOptional`](docs/types.md#utility-types) | TypeScript utility types |
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|--------|---------|-------------|------|
|
|
67
|
-
| `src/zip/sd-zip.ts` | `ZipArchiveProgress`, `ZipArchive` | ZIP archive creation and extraction with progress callback | `sd-zip.spec.ts` |
|
|
61
|
+
---
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
## Utils
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|--------|---------|-------------|------|
|
|
73
|
-
| `src/common.types.ts` | `Bytes`, `PrimitiveTypeMap`, `PrimitiveTypeStr`, `PrimitiveType`, `DeepPartial`, `Type` | Common type utilities (Bytes, PrimitiveType, DeepPartial, Type) | - |
|
|
65
|
+
Pure utility functions for common tasks: formatting, parsing, transformation, and I/O encoding.
|
|
74
66
|
|
|
75
|
-
|
|
67
|
+
[Full documentation](docs/utils.md)
|
|
76
68
|
|
|
77
|
-
|
|
69
|
+
| Symbol | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| [`env`](docs/utils.md#env) | Global environment object from `process.env` |
|
|
72
|
+
| [`formatDate`](docs/utils.md#formatedateformatstring-args) | C#-style date/time format string renderer |
|
|
73
|
+
| [`bytesConcat`](docs/utils.md#bytes-utilities) / [`bytesToHex`](docs/utils.md#bytes-utilities) / [`bytesFromBase64`](docs/utils.md#bytes-utilities) | Binary encoding helpers |
|
|
74
|
+
| [`jsonStringify`](docs/utils.md#json-utilities) / [`jsonParse`](docs/utils.md#json-utilities) | JSON with support for `DateTime`, `Uuid`, `Set`, `Map`, etc. |
|
|
75
|
+
| [`numFormat`](docs/utils.md#number-utilities) / [`numParseInt`](docs/utils.md#number-utilities) | Number formatting and parsing |
|
|
76
|
+
| [`objClone`](docs/utils.md#object-utilities) / [`objEqual`](docs/utils.md#object-utilities) / [`objMerge`](docs/utils.md#object-utilities) | Deep clone, equality, and merge |
|
|
77
|
+
| [`objGetChainValue`](docs/utils.md#object-utilities) / [`objSetChainValue`](docs/utils.md#object-utilities) | Dot-path chain access |
|
|
78
|
+
| [`strToPascalCase`](docs/utils.md#string-utilities) / [`strToKebabCase`](docs/utils.md#string-utilities) | String case conversion |
|
|
79
|
+
| [`koreanGetSuffix`](docs/utils.md#string-utilities) | Korean grammatical particle helper |
|
|
80
|
+
| [`js`](docs/utils.md#template-string-tags) / [`ts`](docs/utils.md#template-string-tags) / [`tsql`](docs/utils.md#template-string-tags) | Template literal syntax-highlighting tags |
|
|
81
|
+
| [`transferableEncode`](docs/utils.md#transferable-utilities) / [`transferableDecode`](docs/utils.md#transferable-utilities) | Web Worker transfer helpers |
|
|
82
|
+
| [`waitUntil`](docs/utils.md#wait-utilities) / [`waitTime`](docs/utils.md#wait-utilities) | Async wait primitives |
|
|
83
|
+
| [`xmlParse`](docs/utils.md#xml-utilities) / [`xmlStringify`](docs/utils.md#xml-utilities) | XML serialization |
|
|
84
|
+
| [`pathJoin`](docs/utils.md#path-utilities) / [`pathBasename`](docs/utils.md#path-utilities) | POSIX path helpers (browser/Capacitor) |
|
|
85
|
+
| [`errorMessage`](docs/utils.md#error-utility) | Safe error-to-string conversion |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Features
|
|
90
|
+
|
|
91
|
+
Async coordination primitives for debouncing, sequential execution, and typed event handling.
|
|
92
|
+
|
|
93
|
+
[Full documentation](docs/features.md)
|
|
94
|
+
|
|
95
|
+
| Symbol | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| [`DebounceQueue`](docs/features.md#debouncequeue) | Executes only the last of rapid-fire calls after a delay |
|
|
98
|
+
| [`SerialQueue`](docs/features.md#serialqueue) | Runs async tasks one at a time in submission order |
|
|
99
|
+
| [`EventEmitter`](docs/features.md#eventemittertevents) | Type-safe event emitter backed by `EventTarget` |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Errors
|
|
104
|
+
|
|
105
|
+
Error classes with structured cause chaining and descriptive formatting.
|
|
106
|
+
|
|
107
|
+
[Full documentation](docs/errors.md)
|
|
108
|
+
|
|
109
|
+
| Symbol | Description |
|
|
110
|
+
|--------|-------------|
|
|
111
|
+
| [`SdError`](docs/errors.md#sderror) | Base error with tree-structured cause chaining |
|
|
112
|
+
| [`ArgumentError`](docs/errors.md#argumenterror) | Invalid argument with YAML-formatted values |
|
|
113
|
+
| [`NotImplementedError`](docs/errors.md#notimplementederror) | Placeholder for unimplemented code paths |
|
|
114
|
+
| [`TimeoutError`](docs/errors.md#timeouterror) | Thrown when a wait loop exceeds its attempt limit |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Zip
|
|
119
|
+
|
|
120
|
+
ZIP archive reading, writing, and compression via `@zip.js/zip.js`.
|
|
121
|
+
|
|
122
|
+
[Full documentation](docs/zip.md)
|
|
123
|
+
|
|
124
|
+
| Symbol | Description |
|
|
125
|
+
|--------|-------------|
|
|
126
|
+
| [`ZipArchive`](docs/zip.md#ziparchive) | Read, write, and compress ZIP archives with `await using` support |
|
|
127
|
+
| [`ZipArchiveProgress`](docs/zip.md#ziparchive) | Progress callback interface for `extractAll` |
|
package/package.json
CHANGED
package/src/utils/str.ts
CHANGED
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* String utility functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
//#region Korean particle handling
|
|
6
|
-
|
|
7
|
-
// Korean particle mapping table (created only once when module loads)
|
|
8
|
-
const suffixTable = {
|
|
9
|
-
을: { t: "을", f: "를" },
|
|
10
|
-
은: { t: "은", f: "는" },
|
|
11
|
-
이: { t: "이", f: "가" },
|
|
12
|
-
와: { t: "과", f: "와" },
|
|
13
|
-
랑: { t: "이랑", f: "랑" },
|
|
14
|
-
로: { t: "으로", f: "로" },
|
|
15
|
-
라: { t: "이라", f: "라" },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Return the appropriate Korean particle based on the final consonant
|
|
20
|
-
* @param text The text to check
|
|
21
|
-
* @param type The particle type
|
|
22
|
-
* - `"을"`: 을/를 (eul/reul - object particle)
|
|
23
|
-
* - `"은"`: 은/는 (eun/neun - subject particle)
|
|
24
|
-
* - `"이"`: 이/가 (i/ga - subject particle)
|
|
25
|
-
* - `"와"`: 과/와 (gwa/wa - and particle)
|
|
26
|
-
* - `"랑"`: 이랑/랑 (irang/rang - and particle)
|
|
27
|
-
* - `"로"`: 으로/로 (euro/ro - instrumental particle)
|
|
28
|
-
* - `"라"`: 이라/라 (ira/ra - copula particle)
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* koreanGetSuffix("Apple", "을") // "를"
|
|
32
|
-
* koreanGetSuffix("책", "이") // "이"
|
|
33
|
-
*/
|
|
34
|
-
export function koreanGetSuffix(
|
|
35
|
-
text: string,
|
|
36
|
-
type: "을" | "은" | "이" | "와" | "랑" | "로" | "라",
|
|
37
|
-
): string {
|
|
38
|
-
const table = suffixTable;
|
|
39
|
-
|
|
40
|
-
if (text.length === 0) {
|
|
41
|
-
return table[type].f;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const lastCharCode = text.charCodeAt(text.length - 1);
|
|
45
|
-
|
|
46
|
-
// Hangul range check (0xAC00 ~ 0xD7A3)
|
|
47
|
-
if (lastCharCode < 0xac00 || lastCharCode > 0xd7a3) {
|
|
48
|
-
return table[type].f;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Determine if final consonant (jongseong) exists
|
|
52
|
-
const jongseongIndex = (lastCharCode - 0xac00) % 28;
|
|
53
|
-
const hasLast = jongseongIndex !== 0;
|
|
54
|
-
|
|
55
|
-
// Special handling for "로" particle: when final consonant is ㄹ (jongseong index 8), use "로"
|
|
56
|
-
if (type === "로" && jongseongIndex === 8) {
|
|
57
|
-
return table[type].f;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return hasLast ? table[type].t : table[type].f;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//#endregion
|
|
64
|
-
|
|
65
|
-
//#region Full-width to half-width conversion
|
|
66
|
-
|
|
67
|
-
// Full-width to half-width mapping table (created only once when module loads)
|
|
68
|
-
const fullWidthCharMap: Record<string, string> = {
|
|
69
|
-
"A": "A",
|
|
70
|
-
"B": "B",
|
|
71
|
-
"C": "C",
|
|
72
|
-
"D": "D",
|
|
73
|
-
"E": "E",
|
|
74
|
-
"F": "F",
|
|
75
|
-
"G": "G",
|
|
76
|
-
"H": "H",
|
|
77
|
-
"I": "I",
|
|
78
|
-
"J": "J",
|
|
79
|
-
"K": "K",
|
|
80
|
-
"L": "L",
|
|
81
|
-
"M": "M",
|
|
82
|
-
"N": "N",
|
|
83
|
-
"O": "O",
|
|
84
|
-
"P": "P",
|
|
85
|
-
"Q": "Q",
|
|
86
|
-
"R": "R",
|
|
87
|
-
"S": "S",
|
|
88
|
-
"T": "T",
|
|
89
|
-
"U": "U",
|
|
90
|
-
"V": "V",
|
|
91
|
-
"W": "W",
|
|
92
|
-
"X": "X",
|
|
93
|
-
"Y": "Y",
|
|
94
|
-
"Z": "Z",
|
|
95
|
-
"a": "a",
|
|
96
|
-
"b": "b",
|
|
97
|
-
"c": "c",
|
|
98
|
-
"d": "d",
|
|
99
|
-
"e": "e",
|
|
100
|
-
"f": "f",
|
|
101
|
-
"g": "g",
|
|
102
|
-
"h": "h",
|
|
103
|
-
"i": "i",
|
|
104
|
-
"j": "j",
|
|
105
|
-
"k": "k",
|
|
106
|
-
"l": "l",
|
|
107
|
-
"m": "m",
|
|
108
|
-
"n": "n",
|
|
109
|
-
"o": "o",
|
|
110
|
-
"p": "p",
|
|
111
|
-
"q": "q",
|
|
112
|
-
"r": "r",
|
|
113
|
-
"s": "s",
|
|
114
|
-
"t": "t",
|
|
115
|
-
"u": "u",
|
|
116
|
-
"v": "v",
|
|
117
|
-
"w": "w",
|
|
118
|
-
"x": "x",
|
|
119
|
-
"y": "y",
|
|
120
|
-
"z": "z",
|
|
121
|
-
"0": "0",
|
|
122
|
-
"1": "1",
|
|
123
|
-
"2": "2",
|
|
124
|
-
"3": "3",
|
|
125
|
-
"4": "4",
|
|
126
|
-
"5": "5",
|
|
127
|
-
"6": "6",
|
|
128
|
-
"7": "7",
|
|
129
|
-
"8": "8",
|
|
130
|
-
"9": "9",
|
|
131
|
-
" ": " ",
|
|
132
|
-
")": ")",
|
|
133
|
-
"(": "(",
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Regex also created only once
|
|
137
|
-
const fullWidthCharRegex = new RegExp(`[${Object.keys(fullWidthCharMap).join("")}]`, "g");
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Convert full-width characters to half-width characters
|
|
141
|
-
*
|
|
142
|
-
* Conversion targets:
|
|
143
|
-
* - Full-width uppercase letters (A-Z → A-Z)
|
|
144
|
-
* - Full-width lowercase letters (a-z → a-z)
|
|
145
|
-
* - Full-width digits (0-9 → 0-9)
|
|
146
|
-
* - Full-width space ( → regular space)
|
|
147
|
-
* - Full-width parentheses (() → ())
|
|
148
|
-
*
|
|
149
|
-
* @example
|
|
150
|
-
* strReplaceFullWidth("A123") // "A123"
|
|
151
|
-
* strReplaceFullWidth("(株)") // "(株)"
|
|
152
|
-
*/
|
|
153
|
-
export function strReplaceFullWidth(str: string): string {
|
|
154
|
-
return str.replace(fullWidthCharRegex, (char) => fullWidthCharMap[char] ?? char);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
//#endregion
|
|
158
|
-
|
|
159
|
-
//#region Case conversion
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Convert to PascalCase
|
|
163
|
-
* @example "hello-world" → "HelloWorld"
|
|
164
|
-
* @example "hello_world" → "HelloWorld"
|
|
165
|
-
* @example "hello.world" → "HelloWorld"
|
|
166
|
-
*/
|
|
167
|
-
export function strToPascalCase(str: string): string {
|
|
168
|
-
return str
|
|
169
|
-
.replace(/[-._][a-z]/g, (m) => m[1].toUpperCase())
|
|
170
|
-
.replace(/^[a-z]/, (m) => m.toUpperCase());
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Convert to camelCase
|
|
175
|
-
* @example "hello-world" → "helloWorld"
|
|
176
|
-
* @example "hello_world" → "helloWorld"
|
|
177
|
-
* @example "HelloWorld" → "helloWorld"
|
|
178
|
-
*/
|
|
179
|
-
export function strToCamelCase(str: string): string {
|
|
180
|
-
return str
|
|
181
|
-
.replace(/[-._][a-z]/g, (m) => m[1].toUpperCase())
|
|
182
|
-
.replace(/^[A-Z]/, (m) => m.toLowerCase());
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Convert to kebab-case
|
|
187
|
-
*
|
|
188
|
-
* @example "HelloWorld" → "hello-world"
|
|
189
|
-
* @example "helloWorld" → "hello-world"
|
|
190
|
-
* @example "hello_world" → "hello_world" (no conversion if only lowercase)
|
|
191
|
-
* @example "Hello_World" → "hello-_world" (existing separators are preserved)
|
|
192
|
-
* @example "Hello-World" → "hello--world" (existing separators are preserved)
|
|
193
|
-
* @example "XMLParser" → "x-m-l-parser" (consecutive uppercase letters are separated)
|
|
194
|
-
*/
|
|
195
|
-
export function strToKebabCase(str: string): string {
|
|
196
|
-
return toCaseWithSeparator(str, "-");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Convert to snake_case
|
|
201
|
-
*
|
|
202
|
-
* @example "HelloWorld" → "hello_world"
|
|
203
|
-
* @example "helloWorld" → "hello_world"
|
|
204
|
-
* @example "hello-world" → "hello-world" (no conversion if only lowercase)
|
|
205
|
-
* @example "Hello-World" → "hello_-world" (existing separators are preserved)
|
|
206
|
-
* @example "Hello_World" → "hello__world" (existing separators are preserved)
|
|
207
|
-
* @example "XMLParser" → "x_m_l_parser" (consecutive uppercase letters are separated)
|
|
208
|
-
*/
|
|
209
|
-
export function strToSnakeCase(str: string): string {
|
|
210
|
-
return toCaseWithSeparator(str, "_");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function toCaseWithSeparator(str: string, separator: string): string {
|
|
214
|
-
return str
|
|
215
|
-
.replace(/^[A-Z]/, (m) => m.toLowerCase())
|
|
216
|
-
.replace(/[-_]?[A-Z]/g, (m) => separator + m.toLowerCase());
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
//#endregion
|
|
220
|
-
|
|
221
|
-
//#region Other
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Check if string is undefined or empty (type guard)
|
|
225
|
-
*
|
|
226
|
-
* @param str The string to check
|
|
227
|
-
* @returns true if undefined, null, or empty string
|
|
228
|
-
*
|
|
229
|
-
* @example
|
|
230
|
-
* const name: string | undefined = getValue();
|
|
231
|
-
* if (strIsNullOrEmpty(name)) {
|
|
232
|
-
* // name: "" | undefined
|
|
233
|
-
* console.log("Name is empty");
|
|
234
|
-
* } else {
|
|
235
|
-
* // name: string (non-empty string)
|
|
236
|
-
* console.log(`Name: ${name}`);
|
|
237
|
-
* }
|
|
238
|
-
*/
|
|
239
|
-
export function strIsNullOrEmpty(str: string | undefined): str is "" | undefined {
|
|
240
|
-
return str == null || str === "";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Insert a string at a specific position
|
|
245
|
-
*
|
|
246
|
-
* @param str The original string
|
|
247
|
-
* @param index The position to insert at (0-based)
|
|
248
|
-
* @param insertString The string to insert
|
|
249
|
-
* @returns A new string with the insertion applied
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* strInsert("Hello World", 5, ","); // "Hello, World"
|
|
253
|
-
* strInsert("abc", 0, "X"); // "Xabc"
|
|
254
|
-
* strInsert("abc", 3, "X"); // "abcX"
|
|
255
|
-
*/
|
|
256
|
-
export function strInsert(str: string, index: number, insertString: string): string {
|
|
257
|
-
return str.substring(0, index) + insertString + str.substring(index);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
//#endregion
|
|
1
|
+
/**
|
|
2
|
+
* String utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
//#region Korean particle handling
|
|
6
|
+
|
|
7
|
+
// Korean particle mapping table (created only once when module loads)
|
|
8
|
+
const suffixTable = {
|
|
9
|
+
을: { t: "을", f: "를" },
|
|
10
|
+
은: { t: "은", f: "는" },
|
|
11
|
+
이: { t: "이", f: "가" },
|
|
12
|
+
와: { t: "과", f: "와" },
|
|
13
|
+
랑: { t: "이랑", f: "랑" },
|
|
14
|
+
로: { t: "으로", f: "로" },
|
|
15
|
+
라: { t: "이라", f: "라" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the appropriate Korean particle based on the final consonant
|
|
20
|
+
* @param text The text to check
|
|
21
|
+
* @param type The particle type
|
|
22
|
+
* - `"을"`: 을/를 (eul/reul - object particle)
|
|
23
|
+
* - `"은"`: 은/는 (eun/neun - subject particle)
|
|
24
|
+
* - `"이"`: 이/가 (i/ga - subject particle)
|
|
25
|
+
* - `"와"`: 과/와 (gwa/wa - and particle)
|
|
26
|
+
* - `"랑"`: 이랑/랑 (irang/rang - and particle)
|
|
27
|
+
* - `"로"`: 으로/로 (euro/ro - instrumental particle)
|
|
28
|
+
* - `"라"`: 이라/라 (ira/ra - copula particle)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* koreanGetSuffix("Apple", "을") // "를"
|
|
32
|
+
* koreanGetSuffix("책", "이") // "이"
|
|
33
|
+
*/
|
|
34
|
+
export function koreanGetSuffix(
|
|
35
|
+
text: string,
|
|
36
|
+
type: "을" | "은" | "이" | "와" | "랑" | "로" | "라",
|
|
37
|
+
): string {
|
|
38
|
+
const table = suffixTable;
|
|
39
|
+
|
|
40
|
+
if (text.length === 0) {
|
|
41
|
+
return table[type].f;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lastCharCode = text.charCodeAt(text.length - 1);
|
|
45
|
+
|
|
46
|
+
// Hangul range check (0xAC00 ~ 0xD7A3)
|
|
47
|
+
if (lastCharCode < 0xac00 || lastCharCode > 0xd7a3) {
|
|
48
|
+
return table[type].f;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Determine if final consonant (jongseong) exists
|
|
52
|
+
const jongseongIndex = (lastCharCode - 0xac00) % 28;
|
|
53
|
+
const hasLast = jongseongIndex !== 0;
|
|
54
|
+
|
|
55
|
+
// Special handling for "로" particle: when final consonant is ㄹ (jongseong index 8), use "로"
|
|
56
|
+
if (type === "로" && jongseongIndex === 8) {
|
|
57
|
+
return table[type].f;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return hasLast ? table[type].t : table[type].f;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
|
|
65
|
+
//#region Full-width to half-width conversion
|
|
66
|
+
|
|
67
|
+
// Full-width to half-width mapping table (created only once when module loads)
|
|
68
|
+
const fullWidthCharMap: Record<string, string> = {
|
|
69
|
+
"A": "A",
|
|
70
|
+
"B": "B",
|
|
71
|
+
"C": "C",
|
|
72
|
+
"D": "D",
|
|
73
|
+
"E": "E",
|
|
74
|
+
"F": "F",
|
|
75
|
+
"G": "G",
|
|
76
|
+
"H": "H",
|
|
77
|
+
"I": "I",
|
|
78
|
+
"J": "J",
|
|
79
|
+
"K": "K",
|
|
80
|
+
"L": "L",
|
|
81
|
+
"M": "M",
|
|
82
|
+
"N": "N",
|
|
83
|
+
"O": "O",
|
|
84
|
+
"P": "P",
|
|
85
|
+
"Q": "Q",
|
|
86
|
+
"R": "R",
|
|
87
|
+
"S": "S",
|
|
88
|
+
"T": "T",
|
|
89
|
+
"U": "U",
|
|
90
|
+
"V": "V",
|
|
91
|
+
"W": "W",
|
|
92
|
+
"X": "X",
|
|
93
|
+
"Y": "Y",
|
|
94
|
+
"Z": "Z",
|
|
95
|
+
"a": "a",
|
|
96
|
+
"b": "b",
|
|
97
|
+
"c": "c",
|
|
98
|
+
"d": "d",
|
|
99
|
+
"e": "e",
|
|
100
|
+
"f": "f",
|
|
101
|
+
"g": "g",
|
|
102
|
+
"h": "h",
|
|
103
|
+
"i": "i",
|
|
104
|
+
"j": "j",
|
|
105
|
+
"k": "k",
|
|
106
|
+
"l": "l",
|
|
107
|
+
"m": "m",
|
|
108
|
+
"n": "n",
|
|
109
|
+
"o": "o",
|
|
110
|
+
"p": "p",
|
|
111
|
+
"q": "q",
|
|
112
|
+
"r": "r",
|
|
113
|
+
"s": "s",
|
|
114
|
+
"t": "t",
|
|
115
|
+
"u": "u",
|
|
116
|
+
"v": "v",
|
|
117
|
+
"w": "w",
|
|
118
|
+
"x": "x",
|
|
119
|
+
"y": "y",
|
|
120
|
+
"z": "z",
|
|
121
|
+
"0": "0",
|
|
122
|
+
"1": "1",
|
|
123
|
+
"2": "2",
|
|
124
|
+
"3": "3",
|
|
125
|
+
"4": "4",
|
|
126
|
+
"5": "5",
|
|
127
|
+
"6": "6",
|
|
128
|
+
"7": "7",
|
|
129
|
+
"8": "8",
|
|
130
|
+
"9": "9",
|
|
131
|
+
" ": " ",
|
|
132
|
+
")": ")",
|
|
133
|
+
"(": "(",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Regex also created only once
|
|
137
|
+
const fullWidthCharRegex = new RegExp(`[${Object.keys(fullWidthCharMap).join("")}]`, "g");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert full-width characters to half-width characters
|
|
141
|
+
*
|
|
142
|
+
* Conversion targets:
|
|
143
|
+
* - Full-width uppercase letters (A-Z → A-Z)
|
|
144
|
+
* - Full-width lowercase letters (a-z → a-z)
|
|
145
|
+
* - Full-width digits (0-9 → 0-9)
|
|
146
|
+
* - Full-width space ( → regular space)
|
|
147
|
+
* - Full-width parentheses (() → ())
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* strReplaceFullWidth("A123") // "A123"
|
|
151
|
+
* strReplaceFullWidth("(株)") // "(株)"
|
|
152
|
+
*/
|
|
153
|
+
export function strReplaceFullWidth(str: string): string {
|
|
154
|
+
return str.replace(fullWidthCharRegex, (char) => fullWidthCharMap[char] ?? char);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
|
|
159
|
+
//#region Case conversion
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert to PascalCase
|
|
163
|
+
* @example "hello-world" → "HelloWorld"
|
|
164
|
+
* @example "hello_world" → "HelloWorld"
|
|
165
|
+
* @example "hello.world" → "HelloWorld"
|
|
166
|
+
*/
|
|
167
|
+
export function strToPascalCase(str: string): string {
|
|
168
|
+
return str
|
|
169
|
+
.replace(/[-._][a-z]/g, (m) => m[1].toUpperCase())
|
|
170
|
+
.replace(/^[a-z]/, (m) => m.toUpperCase());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Convert to camelCase
|
|
175
|
+
* @example "hello-world" → "helloWorld"
|
|
176
|
+
* @example "hello_world" → "helloWorld"
|
|
177
|
+
* @example "HelloWorld" → "helloWorld"
|
|
178
|
+
*/
|
|
179
|
+
export function strToCamelCase(str: string): string {
|
|
180
|
+
return str
|
|
181
|
+
.replace(/[-._][a-z]/g, (m) => m[1].toUpperCase())
|
|
182
|
+
.replace(/^[A-Z]/, (m) => m.toLowerCase());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convert to kebab-case
|
|
187
|
+
*
|
|
188
|
+
* @example "HelloWorld" → "hello-world"
|
|
189
|
+
* @example "helloWorld" → "hello-world"
|
|
190
|
+
* @example "hello_world" → "hello_world" (no conversion if only lowercase)
|
|
191
|
+
* @example "Hello_World" → "hello-_world" (existing separators are preserved)
|
|
192
|
+
* @example "Hello-World" → "hello--world" (existing separators are preserved)
|
|
193
|
+
* @example "XMLParser" → "x-m-l-parser" (consecutive uppercase letters are separated)
|
|
194
|
+
*/
|
|
195
|
+
export function strToKebabCase(str: string): string {
|
|
196
|
+
return toCaseWithSeparator(str, "-");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert to snake_case
|
|
201
|
+
*
|
|
202
|
+
* @example "HelloWorld" → "hello_world"
|
|
203
|
+
* @example "helloWorld" → "hello_world"
|
|
204
|
+
* @example "hello-world" → "hello-world" (no conversion if only lowercase)
|
|
205
|
+
* @example "Hello-World" → "hello_-world" (existing separators are preserved)
|
|
206
|
+
* @example "Hello_World" → "hello__world" (existing separators are preserved)
|
|
207
|
+
* @example "XMLParser" → "x_m_l_parser" (consecutive uppercase letters are separated)
|
|
208
|
+
*/
|
|
209
|
+
export function strToSnakeCase(str: string): string {
|
|
210
|
+
return toCaseWithSeparator(str, "_");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function toCaseWithSeparator(str: string, separator: string): string {
|
|
214
|
+
return str
|
|
215
|
+
.replace(/^[A-Z]/, (m) => m.toLowerCase())
|
|
216
|
+
.replace(/[-_]?[A-Z]/g, (m) => separator + m.toLowerCase());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
|
|
221
|
+
//#region Other
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if string is undefined or empty (type guard)
|
|
225
|
+
*
|
|
226
|
+
* @param str The string to check
|
|
227
|
+
* @returns true if undefined, null, or empty string
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* const name: string | undefined = getValue();
|
|
231
|
+
* if (strIsNullOrEmpty(name)) {
|
|
232
|
+
* // name: "" | undefined
|
|
233
|
+
* console.log("Name is empty");
|
|
234
|
+
* } else {
|
|
235
|
+
* // name: string (non-empty string)
|
|
236
|
+
* console.log(`Name: ${name}`);
|
|
237
|
+
* }
|
|
238
|
+
*/
|
|
239
|
+
export function strIsNullOrEmpty(str: string | undefined): str is "" | undefined {
|
|
240
|
+
return str == null || str === "";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Insert a string at a specific position
|
|
245
|
+
*
|
|
246
|
+
* @param str The original string
|
|
247
|
+
* @param index The position to insert at (0-based)
|
|
248
|
+
* @param insertString The string to insert
|
|
249
|
+
* @returns A new string with the insertion applied
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* strInsert("Hello World", 5, ","); // "Hello, World"
|
|
253
|
+
* strInsert("abc", 0, "X"); // "Xabc"
|
|
254
|
+
* strInsert("abc", 3, "X"); // "abcX"
|
|
255
|
+
*/
|
|
256
|
+
export function strInsert(str: string, index: number, insertString: string): string {
|
|
257
|
+
return str.substring(0, index) + insertString + str.substring(index);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
@@ -1,284 +1,284 @@
|
|
|
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
|
-
* Object types that can be transferred between Workers
|
|
8
|
-
*
|
|
9
|
-
* Only ArrayBuffer is used in this code.
|
|
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 conversion utility functions
|
|
16
|
-
*
|
|
17
|
-
* Performs serialization/deserialization for data transfer between Workers.
|
|
18
|
-
* Handles custom types that structuredClone does not support.
|
|
19
|
-
*
|
|
20
|
-
* Supported types:
|
|
21
|
-
* - Date, DateTime, DateOnly, Time, Uuid, RegExp
|
|
22
|
-
* - Error (including cause, code, detail)
|
|
23
|
-
* - Uint8Array (other TypedArrays not supported, handled as plain objects)
|
|
24
|
-
* - Array, Map, Set, plain objects
|
|
25
|
-
*
|
|
26
|
-
* @note Circular references cause TypeError in transferableEncode (includes path info)
|
|
27
|
-
* @note If the same object is referenced from multiple places, the cached encoding result is reused
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // Send data to Worker
|
|
31
|
-
* const { result, transferList } = transferableEncode(data);
|
|
32
|
-
* worker.postMessage(result, transferList);
|
|
33
|
-
*
|
|
34
|
-
* // Receive data from Worker
|
|
35
|
-
* const decoded = transferableDecode(event.data);
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
//#region encode
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Convert objects using Simplysm types to plain objects
|
|
42
|
-
* Serializes in a form that can be sent to a Worker
|
|
43
|
-
*
|
|
44
|
-
* @throws TypeError if circular reference is detected
|
|
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
|
-
// 객체 타입 processing: 순환 감지 + 캐시
|
|
67
|
-
if (typeof obj === "object") {
|
|
68
|
-
// Circular reference detection (object in current recursion stack)
|
|
69
|
-
if (ancestors.has(obj)) {
|
|
70
|
-
const currentPath = path.length > 0 ? path.join(".") : "root";
|
|
71
|
-
throw new TypeError(`Circular reference detected: ${currentPath}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// If object was already encoded, reuse cached result
|
|
75
|
-
const cached = cache.get(obj);
|
|
76
|
-
if (cached !== undefined) return cached;
|
|
77
|
-
|
|
78
|
-
// Add to recursion stack
|
|
79
|
-
ancestors.add(obj);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let result: unknown;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// 1. Uint8Array
|
|
86
|
-
if (obj instanceof Uint8Array) {
|
|
87
|
-
// SharedArrayBuffer is already shared memory, so don't add to transferList
|
|
88
|
-
// Add only ArrayBuffer to transferList for zero-copy transfer
|
|
89
|
-
const isSharedArrayBuffer =
|
|
90
|
-
typeof SharedArrayBuffer !== "undefined" && obj.buffer instanceof SharedArrayBuffer;
|
|
91
|
-
const buffer = obj.buffer as ArrayBuffer;
|
|
92
|
-
if (!isSharedArrayBuffer && !transferList.includes(buffer)) {
|
|
93
|
-
transferList.push(buffer);
|
|
94
|
-
}
|
|
95
|
-
result = obj;
|
|
96
|
-
}
|
|
97
|
-
// 2. Special type conversion (convert to tagged object without JSON.stringify)
|
|
98
|
-
else if (obj instanceof Date) {
|
|
99
|
-
result = { __type__: "Date", data: obj.getTime() };
|
|
100
|
-
} else if (obj instanceof DateTime) {
|
|
101
|
-
result = { __type__: "DateTime", data: obj.tick };
|
|
102
|
-
} else if (obj instanceof DateOnly) {
|
|
103
|
-
result = { __type__: "DateOnly", data: obj.tick };
|
|
104
|
-
} else if (obj instanceof Time) {
|
|
105
|
-
result = { __type__: "Time", data: obj.tick };
|
|
106
|
-
} else if (obj instanceof Uuid) {
|
|
107
|
-
result = { __type__: "Uuid", data: obj.toString() };
|
|
108
|
-
} else if (obj instanceof RegExp) {
|
|
109
|
-
result = { __type__: "RegExp", data: { source: obj.source, flags: obj.flags } };
|
|
110
|
-
} else if (obj instanceof Error) {
|
|
111
|
-
const errObj = obj as Error & {
|
|
112
|
-
code?: unknown;
|
|
113
|
-
detail?: unknown;
|
|
114
|
-
};
|
|
115
|
-
result = {
|
|
116
|
-
__type__: "Error",
|
|
117
|
-
data: {
|
|
118
|
-
name: errObj.name,
|
|
119
|
-
message: errObj.message,
|
|
120
|
-
stack: errObj.stack,
|
|
121
|
-
...(errObj.code !== undefined ? { code: errObj.code } : {}),
|
|
122
|
-
...(errObj.detail !== undefined
|
|
123
|
-
? {
|
|
124
|
-
detail: encodeImpl(
|
|
125
|
-
errObj.detail,
|
|
126
|
-
transferList,
|
|
127
|
-
[...path, "detail"],
|
|
128
|
-
ancestors,
|
|
129
|
-
cache,
|
|
130
|
-
),
|
|
131
|
-
}
|
|
132
|
-
: {}),
|
|
133
|
-
...(errObj.cause !== undefined
|
|
134
|
-
? {
|
|
135
|
-
cause: encodeImpl(errObj.cause, transferList, [...path, "cause"], ancestors, cache),
|
|
136
|
-
}
|
|
137
|
-
: {}),
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// 3. Array recursion
|
|
142
|
-
else if (Array.isArray(obj)) {
|
|
143
|
-
result = obj.map((item, idx) =>
|
|
144
|
-
encodeImpl(item, transferList, [...path, idx], ancestors, cache),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
// 4. Map recursion
|
|
148
|
-
else if (obj instanceof Map) {
|
|
149
|
-
let idx = 0;
|
|
150
|
-
result = new Map(
|
|
151
|
-
Array.from(obj.entries()).map(([k, v]) => {
|
|
152
|
-
const keyPath = [...path, `Map[${idx}].key`];
|
|
153
|
-
const valuePath = [...path, `Map[${idx}].value`];
|
|
154
|
-
idx++;
|
|
155
|
-
return [
|
|
156
|
-
encodeImpl(k, transferList, keyPath, ancestors, cache),
|
|
157
|
-
encodeImpl(v, transferList, valuePath, ancestors, cache),
|
|
158
|
-
];
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
// 5. Set recursion
|
|
163
|
-
else if (obj instanceof Set) {
|
|
164
|
-
let idx = 0;
|
|
165
|
-
result = new Set(
|
|
166
|
-
Array.from(obj).map((v) =>
|
|
167
|
-
encodeImpl(v, transferList, [...path, `Set[${idx++}]`], ancestors, cache),
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
// 6. Plain object recursion
|
|
172
|
-
else if (typeof obj === "object") {
|
|
173
|
-
const res: Record<string, unknown> = {};
|
|
174
|
-
const record = obj as Record<string, unknown>;
|
|
175
|
-
for (const key of Object.keys(record)) {
|
|
176
|
-
res[key] = encodeImpl(record[key], transferList, [...path, key], ancestors, cache);
|
|
177
|
-
}
|
|
178
|
-
result = res;
|
|
179
|
-
}
|
|
180
|
-
// 7. Primitive types
|
|
181
|
-
else {
|
|
182
|
-
return obj;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Save to cache (only on success)
|
|
186
|
-
if (typeof obj === "object") {
|
|
187
|
-
cache.set(obj, result);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return result;
|
|
191
|
-
} finally {
|
|
192
|
-
// Remove from recursion stack (must execute even on exception)
|
|
193
|
-
if (typeof obj === "object") {
|
|
194
|
-
ancestors.delete(obj);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
//#endregion
|
|
200
|
-
|
|
201
|
-
//#region decode
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Convert serialized objects to objects using Simplysm types
|
|
205
|
-
* Deserialize data received from a Worker
|
|
206
|
-
*/
|
|
207
|
-
export function transferableDecode(obj: unknown): unknown {
|
|
208
|
-
if (obj == null) return obj;
|
|
209
|
-
|
|
210
|
-
// 1. Restore special types from tagged objects
|
|
211
|
-
if (typeof obj === "object" && "__type__" in obj && "data" in obj) {
|
|
212
|
-
const typed = obj as { __type__: string; data: unknown };
|
|
213
|
-
const data = typed.data;
|
|
214
|
-
|
|
215
|
-
if (typed.__type__ === "Date" && typeof data === "number") return new Date(data);
|
|
216
|
-
if (typed.__type__ === "DateTime" && typeof data === "number") return new DateTime(data);
|
|
217
|
-
if (typed.__type__ === "DateOnly" && typeof data === "number") return new DateOnly(data);
|
|
218
|
-
if (typed.__type__ === "Time" && typeof data === "number") return new Time(data);
|
|
219
|
-
if (typed.__type__ === "Uuid" && typeof data === "string") return new Uuid(data);
|
|
220
|
-
if (typed.__type__ === "RegExp" && typeof data === "object" && data !== null) {
|
|
221
|
-
const regexData = data as { source: string; flags: string };
|
|
222
|
-
return new RegExp(regexData.source, regexData.flags);
|
|
223
|
-
}
|
|
224
|
-
if (typed.__type__ === "Error" && typeof data === "object" && data !== null) {
|
|
225
|
-
const errorData = data as {
|
|
226
|
-
name: string;
|
|
227
|
-
message: string;
|
|
228
|
-
stack?: string;
|
|
229
|
-
code?: unknown;
|
|
230
|
-
cause?: unknown;
|
|
231
|
-
detail?: unknown;
|
|
232
|
-
};
|
|
233
|
-
const err = new Error(errorData.message) as Error & {
|
|
234
|
-
code?: unknown;
|
|
235
|
-
detail?: unknown;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
err.name = errorData.name;
|
|
239
|
-
err.stack = errorData.stack;
|
|
240
|
-
|
|
241
|
-
if (errorData.code !== undefined) err.code = errorData.code;
|
|
242
|
-
if (errorData.cause !== undefined) (err as Error).cause = transferableDecode(errorData.cause);
|
|
243
|
-
if (errorData.detail !== undefined) err.detail = transferableDecode(errorData.detail);
|
|
244
|
-
return err;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 2. Array recursion
|
|
249
|
-
if (Array.isArray(obj)) {
|
|
250
|
-
return obj.map((item) => transferableDecode(item));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 3. Map recursion
|
|
254
|
-
if (obj instanceof Map) {
|
|
255
|
-
const newMap = new Map<unknown, unknown>();
|
|
256
|
-
for (const [k, v] of obj) {
|
|
257
|
-
newMap.set(transferableDecode(k), transferableDecode(v));
|
|
258
|
-
}
|
|
259
|
-
return newMap;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 4. Set recursion
|
|
263
|
-
if (obj instanceof Set) {
|
|
264
|
-
const newSet = new Set<unknown>();
|
|
265
|
-
for (const v of obj) {
|
|
266
|
-
newSet.add(transferableDecode(v));
|
|
267
|
-
}
|
|
268
|
-
return newSet;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 5. Object recursion
|
|
272
|
-
if (typeof obj === "object") {
|
|
273
|
-
const record = obj as Record<string, unknown>;
|
|
274
|
-
const result: Record<string, unknown> = {};
|
|
275
|
-
for (const key of Object.keys(record)) {
|
|
276
|
-
result[key] = transferableDecode(record[key]);
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return obj;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
//#endregion
|
|
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
|
+
* Object types that can be transferred between Workers
|
|
8
|
+
*
|
|
9
|
+
* Only ArrayBuffer is used in this code.
|
|
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 conversion utility functions
|
|
16
|
+
*
|
|
17
|
+
* Performs serialization/deserialization for data transfer between Workers.
|
|
18
|
+
* Handles custom types that structuredClone does not support.
|
|
19
|
+
*
|
|
20
|
+
* Supported types:
|
|
21
|
+
* - Date, DateTime, DateOnly, Time, Uuid, RegExp
|
|
22
|
+
* - Error (including cause, code, detail)
|
|
23
|
+
* - Uint8Array (other TypedArrays not supported, handled as plain objects)
|
|
24
|
+
* - Array, Map, Set, plain objects
|
|
25
|
+
*
|
|
26
|
+
* @note Circular references cause TypeError in transferableEncode (includes path info)
|
|
27
|
+
* @note If the same object is referenced from multiple places, the cached encoding result is reused
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Send data to Worker
|
|
31
|
+
* const { result, transferList } = transferableEncode(data);
|
|
32
|
+
* worker.postMessage(result, transferList);
|
|
33
|
+
*
|
|
34
|
+
* // Receive data from Worker
|
|
35
|
+
* const decoded = transferableDecode(event.data);
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
//#region encode
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert objects using Simplysm types to plain objects
|
|
42
|
+
* Serializes in a form that can be sent to a Worker
|
|
43
|
+
*
|
|
44
|
+
* @throws TypeError if circular reference is detected
|
|
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
|
+
// 객체 타입 processing: 순환 감지 + 캐시
|
|
67
|
+
if (typeof obj === "object") {
|
|
68
|
+
// Circular reference detection (object in current recursion stack)
|
|
69
|
+
if (ancestors.has(obj)) {
|
|
70
|
+
const currentPath = path.length > 0 ? path.join(".") : "root";
|
|
71
|
+
throw new TypeError(`Circular reference detected: ${currentPath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If object was already encoded, reuse cached result
|
|
75
|
+
const cached = cache.get(obj);
|
|
76
|
+
if (cached !== undefined) return cached;
|
|
77
|
+
|
|
78
|
+
// Add to recursion stack
|
|
79
|
+
ancestors.add(obj);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let result: unknown;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 1. Uint8Array
|
|
86
|
+
if (obj instanceof Uint8Array) {
|
|
87
|
+
// SharedArrayBuffer is already shared memory, so don't add to transferList
|
|
88
|
+
// Add only ArrayBuffer to transferList for zero-copy transfer
|
|
89
|
+
const isSharedArrayBuffer =
|
|
90
|
+
typeof SharedArrayBuffer !== "undefined" && obj.buffer instanceof SharedArrayBuffer;
|
|
91
|
+
const buffer = obj.buffer as ArrayBuffer;
|
|
92
|
+
if (!isSharedArrayBuffer && !transferList.includes(buffer)) {
|
|
93
|
+
transferList.push(buffer);
|
|
94
|
+
}
|
|
95
|
+
result = obj;
|
|
96
|
+
}
|
|
97
|
+
// 2. Special type conversion (convert to tagged object without JSON.stringify)
|
|
98
|
+
else if (obj instanceof Date) {
|
|
99
|
+
result = { __type__: "Date", data: obj.getTime() };
|
|
100
|
+
} else if (obj instanceof DateTime) {
|
|
101
|
+
result = { __type__: "DateTime", data: obj.tick };
|
|
102
|
+
} else if (obj instanceof DateOnly) {
|
|
103
|
+
result = { __type__: "DateOnly", data: obj.tick };
|
|
104
|
+
} else if (obj instanceof Time) {
|
|
105
|
+
result = { __type__: "Time", data: obj.tick };
|
|
106
|
+
} else if (obj instanceof Uuid) {
|
|
107
|
+
result = { __type__: "Uuid", data: obj.toString() };
|
|
108
|
+
} else if (obj instanceof RegExp) {
|
|
109
|
+
result = { __type__: "RegExp", data: { source: obj.source, flags: obj.flags } };
|
|
110
|
+
} else if (obj instanceof Error) {
|
|
111
|
+
const errObj = obj as Error & {
|
|
112
|
+
code?: unknown;
|
|
113
|
+
detail?: unknown;
|
|
114
|
+
};
|
|
115
|
+
result = {
|
|
116
|
+
__type__: "Error",
|
|
117
|
+
data: {
|
|
118
|
+
name: errObj.name,
|
|
119
|
+
message: errObj.message,
|
|
120
|
+
stack: errObj.stack,
|
|
121
|
+
...(errObj.code !== undefined ? { code: errObj.code } : {}),
|
|
122
|
+
...(errObj.detail !== undefined
|
|
123
|
+
? {
|
|
124
|
+
detail: encodeImpl(
|
|
125
|
+
errObj.detail,
|
|
126
|
+
transferList,
|
|
127
|
+
[...path, "detail"],
|
|
128
|
+
ancestors,
|
|
129
|
+
cache,
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
...(errObj.cause !== undefined
|
|
134
|
+
? {
|
|
135
|
+
cause: encodeImpl(errObj.cause, transferList, [...path, "cause"], ancestors, cache),
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// 3. Array recursion
|
|
142
|
+
else if (Array.isArray(obj)) {
|
|
143
|
+
result = obj.map((item, idx) =>
|
|
144
|
+
encodeImpl(item, transferList, [...path, idx], ancestors, cache),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
// 4. Map recursion
|
|
148
|
+
else if (obj instanceof Map) {
|
|
149
|
+
let idx = 0;
|
|
150
|
+
result = new Map(
|
|
151
|
+
Array.from(obj.entries()).map(([k, v]) => {
|
|
152
|
+
const keyPath = [...path, `Map[${idx}].key`];
|
|
153
|
+
const valuePath = [...path, `Map[${idx}].value`];
|
|
154
|
+
idx++;
|
|
155
|
+
return [
|
|
156
|
+
encodeImpl(k, transferList, keyPath, ancestors, cache),
|
|
157
|
+
encodeImpl(v, transferList, valuePath, ancestors, cache),
|
|
158
|
+
];
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
// 5. Set recursion
|
|
163
|
+
else if (obj instanceof Set) {
|
|
164
|
+
let idx = 0;
|
|
165
|
+
result = new Set(
|
|
166
|
+
Array.from(obj).map((v) =>
|
|
167
|
+
encodeImpl(v, transferList, [...path, `Set[${idx++}]`], ancestors, cache),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
// 6. Plain object recursion
|
|
172
|
+
else if (typeof obj === "object") {
|
|
173
|
+
const res: Record<string, unknown> = {};
|
|
174
|
+
const record = obj as Record<string, unknown>;
|
|
175
|
+
for (const key of Object.keys(record)) {
|
|
176
|
+
res[key] = encodeImpl(record[key], transferList, [...path, key], ancestors, cache);
|
|
177
|
+
}
|
|
178
|
+
result = res;
|
|
179
|
+
}
|
|
180
|
+
// 7. Primitive types
|
|
181
|
+
else {
|
|
182
|
+
return obj;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Save to cache (only on success)
|
|
186
|
+
if (typeof obj === "object") {
|
|
187
|
+
cache.set(obj, result);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
} finally {
|
|
192
|
+
// Remove from recursion stack (must execute even on exception)
|
|
193
|
+
if (typeof obj === "object") {
|
|
194
|
+
ancestors.delete(obj);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
|
|
201
|
+
//#region decode
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Convert serialized objects to objects using Simplysm types
|
|
205
|
+
* Deserialize data received from a Worker
|
|
206
|
+
*/
|
|
207
|
+
export function transferableDecode(obj: unknown): unknown {
|
|
208
|
+
if (obj == null) return obj;
|
|
209
|
+
|
|
210
|
+
// 1. Restore special types from tagged objects
|
|
211
|
+
if (typeof obj === "object" && "__type__" in obj && "data" in obj) {
|
|
212
|
+
const typed = obj as { __type__: string; data: unknown };
|
|
213
|
+
const data = typed.data;
|
|
214
|
+
|
|
215
|
+
if (typed.__type__ === "Date" && typeof data === "number") return new Date(data);
|
|
216
|
+
if (typed.__type__ === "DateTime" && typeof data === "number") return new DateTime(data);
|
|
217
|
+
if (typed.__type__ === "DateOnly" && typeof data === "number") return new DateOnly(data);
|
|
218
|
+
if (typed.__type__ === "Time" && typeof data === "number") return new Time(data);
|
|
219
|
+
if (typed.__type__ === "Uuid" && typeof data === "string") return new Uuid(data);
|
|
220
|
+
if (typed.__type__ === "RegExp" && typeof data === "object" && data !== null) {
|
|
221
|
+
const regexData = data as { source: string; flags: string };
|
|
222
|
+
return new RegExp(regexData.source, regexData.flags);
|
|
223
|
+
}
|
|
224
|
+
if (typed.__type__ === "Error" && typeof data === "object" && data !== null) {
|
|
225
|
+
const errorData = data as {
|
|
226
|
+
name: string;
|
|
227
|
+
message: string;
|
|
228
|
+
stack?: string;
|
|
229
|
+
code?: unknown;
|
|
230
|
+
cause?: unknown;
|
|
231
|
+
detail?: unknown;
|
|
232
|
+
};
|
|
233
|
+
const err = new Error(errorData.message) as Error & {
|
|
234
|
+
code?: unknown;
|
|
235
|
+
detail?: unknown;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
err.name = errorData.name;
|
|
239
|
+
err.stack = errorData.stack;
|
|
240
|
+
|
|
241
|
+
if (errorData.code !== undefined) err.code = errorData.code;
|
|
242
|
+
if (errorData.cause !== undefined) (err as Error).cause = transferableDecode(errorData.cause);
|
|
243
|
+
if (errorData.detail !== undefined) err.detail = transferableDecode(errorData.detail);
|
|
244
|
+
return err;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. Array recursion
|
|
249
|
+
if (Array.isArray(obj)) {
|
|
250
|
+
return obj.map((item) => transferableDecode(item));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 3. Map recursion
|
|
254
|
+
if (obj instanceof Map) {
|
|
255
|
+
const newMap = new Map<unknown, unknown>();
|
|
256
|
+
for (const [k, v] of obj) {
|
|
257
|
+
newMap.set(transferableDecode(k), transferableDecode(v));
|
|
258
|
+
}
|
|
259
|
+
return newMap;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 4. Set recursion
|
|
263
|
+
if (obj instanceof Set) {
|
|
264
|
+
const newSet = new Set<unknown>();
|
|
265
|
+
for (const v of obj) {
|
|
266
|
+
newSet.add(transferableDecode(v));
|
|
267
|
+
}
|
|
268
|
+
return newSet;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 5. Object recursion
|
|
272
|
+
if (typeof obj === "object") {
|
|
273
|
+
const record = obj as Record<string, unknown>;
|
|
274
|
+
const result: Record<string, unknown> = {};
|
|
275
|
+
for (const key of Object.keys(record)) {
|
|
276
|
+
result[key] = transferableDecode(record[key]);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return obj;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|