@simplysm/core-common 13.0.71 → 13.0.74

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 CHANGED
@@ -1,77 +1,127 @@
1
1
  # @simplysm/core-common
2
2
 
3
- Simplysm package - Core module (common)
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
- ## Source Index
11
+ ## Table of Contents
10
12
 
11
- ### (top-level)
13
+ - [Array Extensions](#array-extensions)
14
+ - [Types](#types)
15
+ - [Utils](#utils)
16
+ - [Features](#features)
17
+ - [Errors](#errors)
18
+ - [Zip](#zip)
12
19
 
13
- | Source | Exports | Description | Test |
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
- ### errors
22
+ ## Array Extensions
19
23
 
20
- | Source | Exports | Description | Test |
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
- ### types
26
+ [Full documentation](docs/array-extensions.md)
28
27
 
29
- | Source | Exports | Description | Test |
30
- |--------|---------|-------------|------|
31
- | `src/types/uuid.ts` | `Uuid` | UUID v4 generation and validation utility class | `uuid.spec.ts` |
32
- | `src/types/lazy-gc-map.ts` | `LazyGcMap` | Map with lazy initialization and automatic garbage collection of unused entries | `lazy-gc-map.spec.ts` |
33
- | `src/types/date-time.ts` | `DateTime` | Immutable date-time class with formatting, arithmetic, and comparison | `date-time.spec.ts` |
34
- | `src/types/date-only.ts` | `DateOnly` | Immutable date-only class (year, month, day) without time component | `date-only.spec.ts` |
35
- | `src/types/time.ts` | `Time` | Immutable time-of-day class (hour, minute, second) | `time.spec.ts` |
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
- ### features
43
+ ---
38
44
 
39
- | Source | Exports | Description | Test |
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
- ### utils
47
+ Immutable value types and utility type aliases. All support parsing, formatting, and arithmetic.
46
48
 
47
- | Source | Exports | Description | Test |
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
- ### zip
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
- | Source | Exports | Description | Test |
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
- ### type utilities
63
+ ## Utils
70
64
 
71
- | Source | Exports | Description | Test |
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
- ## License
67
+ [Full documentation](docs/utils.md)
76
68
 
77
- Apache-2.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/core-common",
3
- "version": "13.0.71",
3
+ "version": "13.0.74",
4
4
  "description": "Simplysm package - Core module (common)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@zip.js/zip.js": "^2.8.21",
32
32
  "consola": "^3.4.2",
33
- "fast-xml-parser": "^5.3.7",
33
+ "fast-xml-parser": "^5.4.1",
34
34
  "yaml": "^2.8.2"
35
35
  }
36
36
  }
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