@simplysm/core-common 14.0.1 → 14.0.4

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/docs/errors.md ADDED
@@ -0,0 +1,104 @@
1
+ # Errors
2
+
3
+ A hierarchy of error classes built on `SdError`, which supports tree-structured cause chaining via the ES2024 `cause` property.
4
+
5
+ ## `SdError`
6
+
7
+ Base error class with cause-chain support. Messages are joined in reverse order with ` => `.
8
+
9
+ ```typescript
10
+ class SdError extends Error {
11
+ cause?: Error;
12
+
13
+ constructor(cause: Error, ...messages: string[]);
14
+ constructor(...messages: string[]);
15
+ }
16
+ ```
17
+
18
+ | Constructor Overload | Description |
19
+ |---------------------|-------------|
20
+ | `new SdError(cause, ...messages)` | Wraps an existing error. Message becomes `"messages[n] => ... => messages[0] => cause.message"` |
21
+ | `new SdError(...messages)` | Creates without cause. Message becomes `"messages[n] => ... => messages[0]"` |
22
+
23
+ When a cause error is provided, its stack trace is appended under a `---- cause stack ----` separator.
24
+
25
+ ```typescript
26
+ try {
27
+ await fetch(url);
28
+ } catch (err) {
29
+ throw new SdError(err, "API call failed", "User load failed");
30
+ // message: "User load failed => API call failed => <original message>"
31
+ }
32
+
33
+ throw new SdError("Invalid state", "Cannot process");
34
+ // message: "Cannot process => Invalid state"
35
+ ```
36
+
37
+ ## `ArgumentError`
38
+
39
+ Error for invalid arguments. Extends `SdError`. Formats the argument object as YAML in the message for easy debugging.
40
+
41
+ ```typescript
42
+ class ArgumentError extends SdError {
43
+ constructor(argObj: Record<string, unknown>);
44
+ constructor(message: string, argObj: Record<string, unknown>);
45
+ }
46
+ ```
47
+
48
+ | Constructor Overload | Description |
49
+ |---------------------|-------------|
50
+ | `new ArgumentError(argObj)` | Default message + YAML-formatted args |
51
+ | `new ArgumentError(message, argObj)` | Custom message + YAML-formatted args |
52
+
53
+ ```typescript
54
+ throw new ArgumentError({ userId: 123, name: null });
55
+ // message: "잘못된 인자입니다.\n\nuserId: 123\nname: null"
56
+
57
+ throw new ArgumentError("Invalid user", { userId: 123 });
58
+ // message: "Invalid user\n\nuserId: 123"
59
+ ```
60
+
61
+ ## `NotImplementedError`
62
+
63
+ Error for unimplemented features. Extends `SdError`.
64
+
65
+ ```typescript
66
+ class NotImplementedError extends SdError {
67
+ constructor(message?: string);
68
+ }
69
+ ```
70
+
71
+ | Parameter | Type | Description |
72
+ |-----------|------|-------------|
73
+ | `message` | `string \| undefined` | Optional description of what is not implemented |
74
+
75
+ ```typescript
76
+ throw new NotImplementedError();
77
+ // message: "미구현"
78
+
79
+ throw new NotImplementedError("Subclass must override");
80
+ // message: "미구현: Subclass must override"
81
+ ```
82
+
83
+ ## `TimeoutError`
84
+
85
+ Error for timeout conditions. Extends `SdError`.
86
+
87
+ ```typescript
88
+ class TimeoutError extends SdError {
89
+ constructor(count?: number, message?: string);
90
+ }
91
+ ```
92
+
93
+ | Parameter | Type | Description |
94
+ |-----------|------|-------------|
95
+ | `count` | `number \| undefined` | Number of attempts made |
96
+ | `message` | `string \| undefined` | Additional description |
97
+
98
+ ```typescript
99
+ throw new TimeoutError(50);
100
+ // message: "대기 시간 초과(50회 시도)"
101
+
102
+ throw new TimeoutError(undefined, "API response wait exceeded");
103
+ // message: "대기 시간 초과: API response wait exceeded"
104
+ ```
@@ -0,0 +1,128 @@
1
+ # Features
2
+
3
+ Async queue and event emitter utilities. All support `using` syntax via `Symbol.dispose`.
4
+
5
+ ## `DebounceQueue`
6
+
7
+ Async debounce queue. When called multiple times in rapid succession, only the last enqueued function executes after the delay. Extends `EventEmitter<{ error: SdError }>`.
8
+
9
+ ```typescript
10
+ class DebounceQueue extends EventEmitter<{ error: SdError }> {
11
+ constructor(delay?: number);
12
+
13
+ run(fn: () => void | Promise<void>): void;
14
+ dispose(): void;
15
+ [Symbol.dispose](): void;
16
+ }
17
+ ```
18
+
19
+ ### Constructor
20
+
21
+ | Parameter | Type | Default | Description |
22
+ |-----------|------|---------|-------------|
23
+ | `delay` | `number \| undefined` | `undefined` (next event loop) | Debounce delay in milliseconds |
24
+
25
+ ### Methods
26
+
27
+ | Method | Description |
28
+ |--------|-------------|
29
+ | `run(fn)` | Enqueue a function. Replaces any previously pending function. After the delay, executes the last enqueued function |
30
+ | `dispose()` | Cancels pending timer and clears state |
31
+
32
+ ### Behavior
33
+
34
+ - If a new `run()` call arrives while a previous function is executing, the new function runs immediately after the current one completes (no debounce delay).
35
+ - Errors are emitted as `"error"` events if listeners exist; otherwise logged via consola.
36
+
37
+ ```typescript
38
+ const queue = new DebounceQueue(300);
39
+ queue.on("error", (err) => console.error(err));
40
+
41
+ queue.run(() => console.log("1")); // cancelled
42
+ queue.run(() => console.log("2")); // cancelled
43
+ queue.run(() => console.log("3")); // executes after 300ms
44
+ ```
45
+
46
+ ## `SerialQueue`
47
+
48
+ Async serial queue. Functions are executed one at a time in the order they are enqueued. Errors in one function do not prevent subsequent functions from running. Extends `EventEmitter<{ error: SdError }>`.
49
+
50
+ ```typescript
51
+ class SerialQueue extends EventEmitter<{ error: SdError }> {
52
+ constructor(gap?: number);
53
+
54
+ run(fn: () => void | Promise<void>): void;
55
+ dispose(): void;
56
+ [Symbol.dispose](): void;
57
+ }
58
+ ```
59
+
60
+ ### Constructor
61
+
62
+ | Parameter | Type | Default | Description |
63
+ |-----------|------|---------|-------------|
64
+ | `gap` | `number` | `0` | Delay between each task execution (ms) |
65
+
66
+ ### Methods
67
+
68
+ | Method | Description |
69
+ |--------|-------------|
70
+ | `run(fn)` | Add a function to the queue. Starts processing if not already running |
71
+ | `dispose()` | Clears the pending queue. The currently running task still completes |
72
+
73
+ ```typescript
74
+ const queue = new SerialQueue();
75
+ queue.on("error", (err) => console.error(err));
76
+
77
+ queue.run(async () => { await fetch("/api/1"); }); // runs first
78
+ queue.run(async () => { await fetch("/api/2"); }); // runs after 1 completes
79
+ queue.run(async () => { await fetch("/api/3"); }); // runs after 2 completes
80
+ ```
81
+
82
+ ## `EventEmitter<TEvents>`
83
+
84
+ Type-safe event emitter built on the `EventTarget` API. Works in both browser and Node.js.
85
+
86
+ ```typescript
87
+ class EventEmitter<TEvents extends { [K in keyof TEvents]: unknown } = Record<string, unknown>> {
88
+ on<K extends keyof TEvents & string>(type: K, listener: (data: TEvents[K]) => void): void;
89
+ off<K extends keyof TEvents & string>(type: K, listener: (data: TEvents[K]) => void): void;
90
+ emit<K extends keyof TEvents & string>(
91
+ type: K,
92
+ ...args: TEvents[K] extends void ? [] : [data: TEvents[K]]
93
+ ): void;
94
+ listenerCount<K extends keyof TEvents & string>(type: K): number;
95
+ dispose(): void;
96
+ [Symbol.dispose](): void;
97
+ }
98
+ ```
99
+
100
+ ### Methods
101
+
102
+ | Method | Description |
103
+ |--------|-------------|
104
+ | `on(type, listener)` | Register a listener. Duplicate registration for the same event+listener is ignored |
105
+ | `off(type, listener)` | Remove a listener |
106
+ | `emit(type, data?)` | Dispatch an event. For `void` event types, data argument is omitted |
107
+ | `listenerCount(type)` | Returns the number of registered listeners for an event type |
108
+ | `dispose()` | Removes all listeners from all event types |
109
+
110
+ ### Type Parameter
111
+
112
+ `TEvents` is a map where keys are event names and values are event data types. Use `void` for events with no data.
113
+
114
+ ```typescript
115
+ interface MyEvents {
116
+ data: string;
117
+ error: Error;
118
+ done: void;
119
+ }
120
+
121
+ class MyService extends EventEmitter<MyEvents> {}
122
+
123
+ const svc = new MyService();
124
+ svc.on("data", (data) => {}); // data: string
125
+ svc.on("done", () => {}); // no argument
126
+ svc.emit("data", "hello");
127
+ svc.emit("done"); // no argument needed
128
+ ```
@@ -0,0 +1,91 @@
1
+ # Type Utilities
2
+
3
+ TypeScript type aliases and interfaces exported from `common.types.ts`.
4
+
5
+ ## `Bytes`
6
+
7
+ Type alias for `Uint8Array`. Used throughout the framework instead of `Buffer`.
8
+
9
+ ```typescript
10
+ type Bytes = Uint8Array;
11
+ ```
12
+
13
+ ## `PrimitiveTypeMap`
14
+
15
+ Maps primitive type string keys to their corresponding TypeScript types. Shared with `orm-common`.
16
+
17
+ ```typescript
18
+ type PrimitiveTypeMap = {
19
+ string: string;
20
+ number: number;
21
+ boolean: boolean;
22
+ DateTime: DateTime;
23
+ DateOnly: DateOnly;
24
+ Time: Time;
25
+ Uuid: Uuid;
26
+ Bytes: Bytes;
27
+ };
28
+ ```
29
+
30
+ ## `PrimitiveTypeStr`
31
+
32
+ Union of all keys in `PrimitiveTypeMap`.
33
+
34
+ ```typescript
35
+ type PrimitiveTypeStr = keyof PrimitiveTypeMap;
36
+ // "string" | "number" | "boolean" | "DateTime" | "DateOnly" | "Time" | "Uuid" | "Bytes"
37
+ ```
38
+
39
+ ## `PrimitiveType`
40
+
41
+ Union of all primitive type values (including `undefined`).
42
+
43
+ ```typescript
44
+ type PrimitiveType = PrimitiveTypeMap[PrimitiveTypeStr] | undefined;
45
+ // string | number | boolean | DateTime | DateOnly | Time | Uuid | Uint8Array | undefined
46
+ ```
47
+
48
+ ## `DeepPartial<TObject>`
49
+
50
+ Recursively makes all properties optional. Primitive types (`PrimitiveType`) are preserved as-is; only object/array types are recursively made partial.
51
+
52
+ ```typescript
53
+ type DeepPartial<TObject> = Partial<{
54
+ [K in keyof TObject]: TObject[K] extends PrimitiveType
55
+ ? TObject[K]
56
+ : DeepPartial<TObject[K]>;
57
+ }>;
58
+ ```
59
+
60
+ ```typescript
61
+ interface User {
62
+ name: string;
63
+ profile: {
64
+ age: number;
65
+ address: { city: string };
66
+ };
67
+ }
68
+
69
+ const partial: DeepPartial<User> = {
70
+ profile: { address: {} },
71
+ };
72
+ ```
73
+
74
+ ## `Type<TInstance>`
75
+
76
+ Constructor type interface. Represents a class constructor that creates instances of `TInstance`. Used for dependency injection, factory patterns, and `instanceof` checks.
77
+
78
+ ```typescript
79
+ interface Type<TInstance> extends Function {
80
+ new (...args: unknown[]): TInstance;
81
+ }
82
+ ```
83
+
84
+ ```typescript
85
+ function create<T>(ctor: Type<T>): T {
86
+ return new ctor();
87
+ }
88
+
89
+ class MyClass { name = "test"; }
90
+ const instance = create(MyClass); // MyClass instance
91
+ ```
package/docs/types.md ADDED
@@ -0,0 +1,307 @@
1
+ # Types
2
+
3
+ Immutable value types for UUID, date/time, and a self-expiring map.
4
+
5
+ ## `Uuid`
6
+
7
+ UUID v4 class. Uses `crypto.getRandomValues` for cryptographically secure generation.
8
+
9
+ ```typescript
10
+ class Uuid {
11
+ static generate(): Uuid;
12
+ static fromBytes(bytes: Bytes): Uuid;
13
+
14
+ constructor(uuid: string);
15
+
16
+ toString(): string;
17
+ toBytes(): Bytes;
18
+ }
19
+ ```
20
+
21
+ ### Static Methods
22
+
23
+ | Method | Description |
24
+ |--------|-------------|
25
+ | `generate()` | Creates a new random UUID v4 instance |
26
+ | `fromBytes(bytes)` | Creates a UUID from a 16-byte `Uint8Array`. Throws `ArgumentError` if length is not 16 |
27
+
28
+ ### Constructor
29
+
30
+ | Parameter | Type | Description |
31
+ |-----------|------|-------------|
32
+ | `uuid` | `string` | UUID string in format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. Throws `ArgumentError` if invalid |
33
+
34
+ ### Instance Methods
35
+
36
+ | Method | Returns | Description |
37
+ |--------|---------|-------------|
38
+ | `toString()` | `string` | Returns the UUID string representation |
39
+ | `toBytes()` | `Bytes` | Converts to a 16-byte `Uint8Array` |
40
+
41
+ ```typescript
42
+ const id = Uuid.generate();
43
+ const fromStr = new Uuid("550e8400-e29b-41d4-a716-446655440000");
44
+ const bytes = id.toBytes(); // Uint8Array(16)
45
+ const restored = Uuid.fromBytes(bytes);
46
+ ```
47
+
48
+ ## `LazyGcMap<TKey, TValue>`
49
+
50
+ A Map with automatic expiration. Entries not accessed within `expireTime` are garbage collected. Supports `using` syntax (Symbol.dispose).
51
+
52
+ ```typescript
53
+ class LazyGcMap<TKey, TValue> {
54
+ constructor(options: {
55
+ gcInterval?: number;
56
+ expireTime: number;
57
+ onExpire?: (key: TKey, value: TValue) => void | Promise<void>;
58
+ });
59
+
60
+ get size(): number;
61
+ has(key: TKey): boolean;
62
+ get(key: TKey): TValue | undefined;
63
+ set(key: TKey, value: TValue): void;
64
+ delete(key: TKey): boolean;
65
+ getOrCreate(key: TKey, factory: () => TValue): TValue;
66
+ clear(): void;
67
+ dispose(): void;
68
+ [Symbol.dispose](): void;
69
+
70
+ values(): IterableIterator<TValue>;
71
+ keys(): IterableIterator<TKey>;
72
+ entries(): IterableIterator<[TKey, TValue]>;
73
+ }
74
+ ```
75
+
76
+ ### Constructor Options
77
+
78
+ | Option | Type | Default | Description |
79
+ |--------|------|---------|-------------|
80
+ | `gcInterval` | `number` | `expireTime / 10` (min 1000ms) | Interval between GC runs (ms) |
81
+ | `expireTime` | `number` | required | Time since last access before entry is removed (ms) |
82
+ | `onExpire` | `(key, value) => void \| Promise<void>` | `undefined` | Callback when an entry expires |
83
+
84
+ ### Methods
85
+
86
+ | Method | Description |
87
+ |--------|-------------|
88
+ | `has(key)` | Check if key exists (does NOT refresh access time) |
89
+ | `get(key)` | Get value and refresh access time (LRU) |
90
+ | `set(key, value)` | Set value and start GC timer if needed |
91
+ | `delete(key)` | Delete entry; stops GC if map becomes empty |
92
+ | `getOrCreate(key, factory)` | Get or create value. Throws if disposed |
93
+ | `clear()` | Remove all entries (instance remains usable) |
94
+ | `dispose()` | Stop GC timer and clear all data. Instance becomes unusable |
95
+
96
+ **Important:** Always call `dispose()` or use `using` to prevent memory leaks from the GC timer.
97
+
98
+ ```typescript
99
+ using map = new LazyGcMap<string, Connection>({
100
+ expireTime: 60_000,
101
+ onExpire: async (_key, conn) => { await conn.close(); },
102
+ });
103
+
104
+ map.set("conn1", createConnection());
105
+ const conn = map.get("conn1"); // refreshes access time
106
+ ```
107
+
108
+ ## `DateTime`
109
+
110
+ Immutable date-time class wrapping JavaScript `Date`. Millisecond precision, local timezone.
111
+
112
+ ```typescript
113
+ class DateTime {
114
+ readonly date: Date;
115
+
116
+ constructor();
117
+ constructor(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number);
118
+ constructor(tick: number);
119
+ constructor(date: Date);
120
+
121
+ static parse(str: string): DateTime;
122
+
123
+ // Getters
124
+ get year(): number;
125
+ get month(): number; // 1-12
126
+ get day(): number;
127
+ get hour(): number;
128
+ get minute(): number;
129
+ get second(): number;
130
+ get millisecond(): number;
131
+ get tick(): number;
132
+ get dayOfWeek(): number; // 0(Sun)-6(Sat)
133
+ get timezoneOffsetMinutes(): number;
134
+ get isValid(): boolean;
135
+
136
+ // Immutable setters (return new instance)
137
+ setYear(year: number): DateTime;
138
+ setMonth(month: number): DateTime;
139
+ setDay(day: number): DateTime;
140
+ setHour(hour: number): DateTime;
141
+ setMinute(minute: number): DateTime;
142
+ setSecond(second: number): DateTime;
143
+ setMillisecond(millisecond: number): DateTime;
144
+
145
+ // Arithmetic (return new instance)
146
+ addYears(years: number): DateTime;
147
+ addMonths(months: number): DateTime;
148
+ addDays(days: number): DateTime;
149
+ addHours(hours: number): DateTime;
150
+ addMinutes(minutes: number): DateTime;
151
+ addSeconds(seconds: number): DateTime;
152
+ addMilliseconds(milliseconds: number): DateTime;
153
+
154
+ // Formatting
155
+ toFormatString(formatStr: string): string;
156
+ toString(): string; // "yyyy-MM-ddTHH:mm:ss.fffzzz"
157
+ }
158
+ ```
159
+
160
+ ### `DateTime.parse` Supported Formats
161
+
162
+ | Format | Example |
163
+ |--------|---------|
164
+ | `yyyy-MM-dd HH:mm:ss` | `"2025-01-15 10:30:00"` |
165
+ | `yyyy-MM-dd HH:mm:ss.fff` | `"2025-01-15 10:30:00.123"` |
166
+ | `yyyyMMddHHmmss` | `"20250115103000"` |
167
+ | `yyyy-MM-dd AM/PM HH:mm:ss` | `"2025-01-15 AM 10:30:00"` |
168
+ | ISO 8601 | `"2025-01-15T10:30:00Z"` |
169
+ | Korean AM/PM | `"2025-01-15 오전 10:30:00"` |
170
+
171
+ ```typescript
172
+ const now = new DateTime();
173
+ const d = new DateTime(2025, 1, 15, 10, 30, 0);
174
+ const parsed = DateTime.parse("2025-01-15 10:30:00");
175
+ const next = d.addDays(7).setHour(14);
176
+ d.toFormatString("yyyy-MM-dd HH:mm"); // "2025-01-15 10:30"
177
+ ```
178
+
179
+ ## `DateOnly`
180
+
181
+ Immutable date class (no time component). Local timezone.
182
+
183
+ ```typescript
184
+ class DateOnly {
185
+ readonly date: Date;
186
+
187
+ constructor();
188
+ constructor(year: number, month: number, day: number);
189
+ constructor(tick: number);
190
+ constructor(date: Date);
191
+
192
+ static parse(str: string): DateOnly;
193
+ static getDateByYearWeekSeq(
194
+ arg: { year: number; month?: number; weekSeq: number },
195
+ weekStartDay?: number,
196
+ minDaysInFirstWeek?: number,
197
+ ): DateOnly;
198
+
199
+ // Getters
200
+ get year(): number;
201
+ get month(): number; // 1-12
202
+ get day(): number;
203
+ get tick(): number;
204
+ get dayOfWeek(): number; // 0(Sun)-6(Sat)
205
+ get isValid(): boolean;
206
+
207
+ // Immutable setters
208
+ setYear(year: number): DateOnly;
209
+ setMonth(month: number): DateOnly;
210
+ setDay(day: number): DateOnly;
211
+
212
+ // Arithmetic
213
+ addYears(years: number): DateOnly;
214
+ addMonths(months: number): DateOnly;
215
+ addDays(days: number): DateOnly;
216
+
217
+ // Week calculations
218
+ getBaseYearMonthSeqForWeekSeq(weekStartDay?: number, minDaysInFirstWeek?: number): { year: number; monthSeq: number };
219
+ getWeekSeqStartDate(weekStartDay?: number, minDaysInFirstWeek?: number): DateOnly;
220
+ getWeekSeqOfYear(weekStartDay?: number, minDaysInFirstWeek?: number): { year: number; weekSeq: number };
221
+ getWeekSeqOfMonth(weekStartDay?: number, minDaysInFirstWeek?: number): { year: number; monthSeq: number; weekSeq: number };
222
+
223
+ // Formatting
224
+ toFormatString(formatStr: string): string;
225
+ toString(): string; // "yyyy-MM-dd"
226
+ }
227
+ ```
228
+
229
+ ### `DateOnly.parse` Supported Formats
230
+
231
+ | Format | Example |
232
+ |--------|---------|
233
+ | `yyyy-MM-dd` | `"2025-01-15"` (timezone-safe) |
234
+ | `yyyyMMdd` | `"20250115"` (timezone-safe) |
235
+ | ISO 8601 | `"2025-01-15T00:00:00Z"` (converted to local) |
236
+
237
+ ### Week Calculation Parameters
238
+
239
+ | Parameter | Default | Description |
240
+ |-----------|---------|-------------|
241
+ | `weekStartDay` | `1` (Monday) | Week start day: 0=Sun, 1=Mon, ..., 6=Sat |
242
+ | `minDaysInFirstWeek` | `4` (ISO 8601) | Minimum days in the first week |
243
+
244
+ ```typescript
245
+ const today = new DateOnly();
246
+ const d = new DateOnly(2025, 1, 15);
247
+ const parsed = DateOnly.parse("2025-01-15");
248
+ d.getWeekSeqOfYear(); // { year: 2025, weekSeq: 3 }
249
+ d.getWeekSeqOfMonth(); // { year: 2025, monthSeq: 1, weekSeq: 3 }
250
+ DateOnly.getDateByYearWeekSeq({ year: 2025, weekSeq: 2 }); // 2025-01-06
251
+ ```
252
+
253
+ ## `Time`
254
+
255
+ Immutable time class (no date component). Values automatically wrap within 24 hours.
256
+
257
+ ```typescript
258
+ class Time {
259
+ constructor();
260
+ constructor(hour: number, minute: number, second?: number, millisecond?: number);
261
+ constructor(tick: number);
262
+ constructor(date: Date);
263
+
264
+ static parse(str: string): Time;
265
+
266
+ // Getters
267
+ get hour(): number;
268
+ get minute(): number;
269
+ get second(): number;
270
+ get millisecond(): number;
271
+ get tick(): number;
272
+ get isValid(): boolean;
273
+
274
+ // Immutable setters
275
+ setHour(hour: number): Time;
276
+ setMinute(minute: number): Time;
277
+ setSecond(second: number): Time;
278
+ setMillisecond(millisecond: number): Time;
279
+
280
+ // Arithmetic (wraps at 24h)
281
+ addHours(hours: number): Time;
282
+ addMinutes(minutes: number): Time;
283
+ addSeconds(seconds: number): Time;
284
+ addMilliseconds(milliseconds: number): Time;
285
+
286
+ // Formatting
287
+ toFormatString(formatStr: string): string;
288
+ toString(): string; // "HH:mm:ss.fff"
289
+ }
290
+ ```
291
+
292
+ ### `Time.parse` Supported Formats
293
+
294
+ | Format | Example |
295
+ |--------|---------|
296
+ | `HH:mm:ss` | `"10:30:00"` |
297
+ | `HH:mm:ss.fff` | `"10:30:00.123"` |
298
+ | `AM/PM HH:mm:ss` | `"AM 10:30:00"` |
299
+ | ISO 8601 (time part) | `"2025-01-15T10:30:00Z"` |
300
+
301
+ ```typescript
302
+ const now = new Time();
303
+ const t = new Time(14, 30, 0);
304
+ const parsed = Time.parse("10:30:00");
305
+ t.addHours(12); // wraps: 02:30:00
306
+ t.toFormatString("tt h:mm:ss"); // "PM 2:30:00"
307
+ ```