@pvorona/failable 0.7.0 → 0.9.0
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 +219 -33
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -187
- package/dist/lib/failable.d.ts +139 -65
- package/dist/lib/failable.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ A `Failable<T, E>` is either `Success<T>` or `Failure<E>`.
|
|
|
11
11
|
- `success()` / `success(data)` / `failure()` / `failure(error)` create results
|
|
12
12
|
- `failable(...)` captures thrown or rejected boundaries
|
|
13
13
|
- `run(...)` composes multiple `Failable` steps
|
|
14
|
+
- `all(...)`, `allSettled(...)`, and `race(...)` combine multiple sources
|
|
15
|
+
- `result.map(...)` / `result.flatMap(...)` transform and chain success values
|
|
14
16
|
|
|
15
17
|
## Install
|
|
16
18
|
|
|
@@ -67,9 +69,12 @@ if (result.isFailure) {
|
|
|
67
69
|
| Read the value or provide a fallback | `getOr(...)` / `getOrElse(...)` |
|
|
68
70
|
| Recover to `Success<T>` | `or(...)` / `orElse(...)` |
|
|
69
71
|
| Map both branches to one output | `match(onSuccess, onFailure)` |
|
|
70
|
-
| Throw
|
|
72
|
+
| Throw an `Error` from a failure | `getOrThrow(normalizeOption?)` / `throwIfFailure(result, normalizeOption?)` |
|
|
71
73
|
| Capture a throwing or rejecting boundary | `failable(...)` |
|
|
72
74
|
| Compose multiple `Failable` steps | `run(...)` |
|
|
75
|
+
| Combine multiple `Failable` sources | `all(...)`, `allSettled(...)`, `race(...)` |
|
|
76
|
+
| Transform a successful value only | `map(...)` |
|
|
77
|
+
| Chain another `Failable` step | `flatMap(...)` |
|
|
73
78
|
| Cross a structured-clone boundary | `toFailableLike(...)` + `failable(...)` |
|
|
74
79
|
| Validate `unknown` input | `isFailable(...)`, `isSuccess(...)`, `isFailure(...)`, `isFailableLike(...)` |
|
|
75
80
|
|
|
@@ -79,14 +84,36 @@ Start with ordinary branching on `result.isFailure` or `result.isSuccess`. When
|
|
|
79
84
|
you want something shorter, use the helper that matches the job:
|
|
80
85
|
|
|
81
86
|
- `result.getOr(fallback)`: return the success value or an eager fallback
|
|
82
|
-
- `result.getOrElse(() => fallback)`:
|
|
87
|
+
- `result.getOrElse(() => fallback)`: lazy fallback
|
|
88
|
+
- `result.getOrElse((error) => fallback)`: lazy fallback derived from the failure
|
|
83
89
|
- `result.or(fallback)`: recover to `Success<T>` with an eager fallback
|
|
84
|
-
- `result.orElse(() => fallback)`:
|
|
90
|
+
- `result.orElse(() => fallback)`: lazy recovery to `Success<T>`
|
|
91
|
+
- `result.orElse((error) => fallback)`: lazy recovery to `Success<T>` derived from the failure
|
|
85
92
|
- `result.match(onSuccess, onFailure)`: map both branches to one output
|
|
86
|
-
- `result.getOrThrow()`: return the success value or throw `
|
|
87
|
-
- `
|
|
93
|
+
- `result.getOrThrow(normalizeOption?)`: return the success value or throw an `Error` derived from the failure
|
|
94
|
+
- `throwIfFailure(result, normalizeOption?)`: throw an `Error` derived from the failure and narrow the same variable
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
Both throw helpers preserve existing `Error` instances unchanged by default.
|
|
97
|
+
Other failure values are normalized with the built-in rules: arrays become
|
|
98
|
+
`AggregateError`; plain objects become `Error` with `cause`; primitives and
|
|
99
|
+
`undefined` become `Error(String(value), { cause: value })`.
|
|
100
|
+
|
|
101
|
+
Pass `NormalizedErrors` or a custom `normalizeError(...)` when you need a
|
|
102
|
+
specific `Error` shape at the throw boundary. Normalize earlier with
|
|
103
|
+
`failable(...)` only when you need that normalized `Error` inside the
|
|
104
|
+
`Failure` channel before anything throws. If built-in message derivation
|
|
105
|
+
itself fails, normalization still returns an `Error` with message
|
|
106
|
+
`Unstringifiable error value` and `cause` set to the original raw value.
|
|
107
|
+
|
|
108
|
+
Use the lazy forms when the fallback is expensive or has side effects. Failure
|
|
109
|
+
callbacks receive the stored error, so `() => ...` can ignore it and
|
|
110
|
+
`(error) => ...` can use it:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const port = result.getOrElse((error) => {
|
|
114
|
+
return error.code === 'missing' ? 3000 : 8080;
|
|
115
|
+
});
|
|
116
|
+
```
|
|
90
117
|
|
|
91
118
|
Using `readPort` from above:
|
|
92
119
|
|
|
@@ -100,18 +127,83 @@ const label = result.match(
|
|
|
100
127
|
);
|
|
101
128
|
```
|
|
102
129
|
|
|
103
|
-
`
|
|
130
|
+
`throwIfFailure` narrows the result to `Success` in place, so
|
|
104
131
|
subsequent code can access `.data` without branching:
|
|
105
132
|
|
|
106
133
|
```ts
|
|
107
|
-
import {
|
|
134
|
+
import { throwIfFailure } from '@pvorona/failable';
|
|
108
135
|
|
|
109
136
|
const result = readPort(process.env.PORT);
|
|
110
137
|
|
|
111
|
-
|
|
138
|
+
throwIfFailure(result);
|
|
112
139
|
console.log(result.data * 2);
|
|
113
140
|
```
|
|
114
141
|
|
|
142
|
+
When you want a specific `Error` shape only at the throw site, pass the
|
|
143
|
+
normalize option there:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { NormalizedErrors } from '@pvorona/failable';
|
|
147
|
+
|
|
148
|
+
const port = readPort(process.env.PORT).getOrThrow(NormalizedErrors);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Transform And Chain With `map(...)` And `flatMap(...)`
|
|
152
|
+
|
|
153
|
+
Use `result.map(fn)` when you only need to change the success value. The callback
|
|
154
|
+
runs on `Success` only; on `Failure`, the same failure is returned unchanged.
|
|
155
|
+
|
|
156
|
+
Use `result.flatMap(fn)` when the next step can fail again. The callback must
|
|
157
|
+
return another `Failable`. On `Success`, that result becomes the outcome; on
|
|
158
|
+
`Failure`, `flatMap` short-circuits and keeps the original error.
|
|
159
|
+
|
|
160
|
+
Building on `readPort` from [Basic Usage](#basic-usage):
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { failure, success, type Failable } from '@pvorona/failable';
|
|
164
|
+
|
|
165
|
+
type ReadPortError =
|
|
166
|
+
| { code: 'missing' }
|
|
167
|
+
| { code: 'invalid'; raw: string };
|
|
168
|
+
|
|
169
|
+
type ApplicationPortError =
|
|
170
|
+
| ReadPortError
|
|
171
|
+
| { code: 'not_application_port'; port: number };
|
|
172
|
+
|
|
173
|
+
function readPort(raw: string | undefined): Failable<number, ReadPortError> {
|
|
174
|
+
if (raw === undefined) return failure({ code: 'missing' });
|
|
175
|
+
|
|
176
|
+
const port = Number(raw);
|
|
177
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
178
|
+
return failure({ code: 'invalid', raw });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return success(port);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ensureApplicationPort(
|
|
185
|
+
port: number,
|
|
186
|
+
): Failable<number, ApplicationPortError> {
|
|
187
|
+
if (port < 3000 || port > 3999) {
|
|
188
|
+
return failure({ code: 'not_application_port', port });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return success(port);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const appPortResult = readPort(process.env.PORT).flatMap((port) =>
|
|
195
|
+
ensureApplicationPort(port),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const labelResult = appPortResult.map(
|
|
199
|
+
(port) => `Application listening on ${port}`,
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
When you pass object literals directly into `success(...)` or `failure(...)`,
|
|
204
|
+
TypeScript often keeps their types as narrow as possible (literal fields where
|
|
205
|
+
that makes sense), which helps `switch` on `error.code` and similar patterns.
|
|
206
|
+
|
|
115
207
|
## Capture Thrown Or Rejected Failures With `failable(...)`
|
|
116
208
|
|
|
117
209
|
Use `failable(...)` at a boundary you do not control. It turns a thrown or
|
|
@@ -134,7 +226,8 @@ if (configResult.isFailure) {
|
|
|
134
226
|
```
|
|
135
227
|
|
|
136
228
|
`NormalizedErrors` is the built-in shortcut when you want `.error` to be an
|
|
137
|
-
`Error
|
|
229
|
+
`Error`, including when the thrown or rejected non-`Error` value cannot be
|
|
230
|
+
stringified safely.
|
|
138
231
|
|
|
139
232
|
Pass a promise directly when you want rejection capture:
|
|
140
233
|
|
|
@@ -155,20 +248,34 @@ const config = fileResult.getOr('{}');
|
|
|
155
248
|
- preserve an existing `Failable`
|
|
156
249
|
- rehydrate a `FailableLike`
|
|
157
250
|
- capture sync throws from a callback
|
|
158
|
-
- capture promise rejections from a promise
|
|
251
|
+
- capture promise rejections from a promise passed directly
|
|
159
252
|
- normalize failures with `NormalizedErrors` or a custom `normalizeError(...)`
|
|
160
253
|
|
|
161
254
|
By default, the thrown or rejected value becomes `.error` unchanged.
|
|
162
255
|
|
|
163
|
-
Pass the promise itself when you want rejection capture.
|
|
164
|
-
`
|
|
165
|
-
|
|
256
|
+
Pass the promise itself when you want rejection capture. In TypeScript,
|
|
257
|
+
obviously promise-returning callbacks like `async () => ...` and
|
|
258
|
+
`() => Promise.resolve(...)` are rejected. JS callers, plus `any`/`unknown`-typed
|
|
259
|
+
callbacks, receive a `Failure<Error>` telling them to pass the promise directly
|
|
260
|
+
instead.
|
|
166
261
|
|
|
167
262
|
## Compose Existing `Failable` Steps With `run(...)`
|
|
168
263
|
|
|
169
264
|
Use `run(...)` when each step already returns `Failable` and you want to write
|
|
170
|
-
the success path once. If any yielded step fails,
|
|
171
|
-
|
|
265
|
+
the success path once. If any yielded step fails, that failure becomes the
|
|
266
|
+
default unwind result. Cleanup still runs first, and an explicit `return`
|
|
267
|
+
reached in `finally` overrides it. Yielded cleanup `Failure` values keep the
|
|
268
|
+
current unwind result unless a later cleanup `return` overrides it.
|
|
269
|
+
|
|
270
|
+
Inside a `run(...)` builder, there are two valid delegation forms:
|
|
271
|
+
|
|
272
|
+
- `yield* result` when `result` is already a hydrated `Failable`
|
|
273
|
+
- `yield* await promisedResult` in async builders when you have a
|
|
274
|
+
`Promise<Failable<...>>`
|
|
275
|
+
|
|
276
|
+
Hydrated `Failable` values expose sync and async iterators so `run(...)` can
|
|
277
|
+
intercept `yield* result` in both sync and async builders. Outside `run(...)`,
|
|
278
|
+
treat them as result objects rather than as a general-purpose collection API.
|
|
172
279
|
|
|
173
280
|
Without `run(...)`, composing steps means checking each result before
|
|
174
281
|
continuing:
|
|
@@ -233,16 +340,20 @@ function loadConfig(
|
|
|
233
340
|
}
|
|
234
341
|
```
|
|
235
342
|
|
|
236
|
-
When a helper already returns a hydrated
|
|
237
|
-
`yield* helper()`.
|
|
238
|
-
|
|
343
|
+
When a helper already returns a hydrated `Failable`, yield it directly with
|
|
344
|
+
`yield* helper()`. For promised sources in async builders, await them first and
|
|
345
|
+
then yield the hydrated result with `yield* await promisedHelper()`.
|
|
346
|
+
|
|
347
|
+
`run(...)` does not inject helper arguments. Import the top-level combinators
|
|
348
|
+
you need and use them directly inside the builder.
|
|
239
349
|
|
|
240
350
|
For async flows, switch to `run(async function* ...)`. Sync hydrated helpers
|
|
241
|
-
still work with direct `yield* helper()`,
|
|
242
|
-
|
|
351
|
+
still work with direct `yield* helper()`, and promised sources compose with
|
|
352
|
+
`yield* await ...`:
|
|
243
353
|
|
|
244
354
|
```ts
|
|
245
355
|
import {
|
|
356
|
+
all,
|
|
246
357
|
failable,
|
|
247
358
|
failure,
|
|
248
359
|
run,
|
|
@@ -261,17 +372,17 @@ type Profile = { id: string; pictureUrl: string };
|
|
|
261
372
|
async function readJson<T>(url: string) {
|
|
262
373
|
const responseResult = await failable(fetch(url));
|
|
263
374
|
if (responseResult.isFailure) {
|
|
264
|
-
return failure({ code: 'network_error', cause: responseResult.error }
|
|
375
|
+
return failure({ code: 'network_error', cause: responseResult.error });
|
|
265
376
|
}
|
|
266
377
|
|
|
267
378
|
const response = responseResult.data;
|
|
268
379
|
if (!response.ok) {
|
|
269
|
-
return failure({ code: 'http_error', status: response.status }
|
|
380
|
+
return failure({ code: 'http_error', status: response.status });
|
|
270
381
|
}
|
|
271
382
|
|
|
272
383
|
const jsonResult = await failable(response.json());
|
|
273
384
|
if (jsonResult.isFailure) {
|
|
274
|
-
return failure({ code: 'json_parse_error', cause: jsonResult.error }
|
|
385
|
+
return failure({ code: 'json_parse_error', cause: jsonResult.error });
|
|
275
386
|
}
|
|
276
387
|
|
|
277
388
|
return success(jsonResult.data as T);
|
|
@@ -288,22 +399,94 @@ async function getUserProfile(userId: string) {
|
|
|
288
399
|
async function loadUserPage(
|
|
289
400
|
userId: string,
|
|
290
401
|
): Promise<Failable<{ user: User; profile: Profile }, ApiError>> {
|
|
291
|
-
return await run(async function* (
|
|
292
|
-
const user = yield*
|
|
293
|
-
|
|
402
|
+
return await run(async function* () {
|
|
403
|
+
const [user, profile] = yield* await all(
|
|
404
|
+
getUser(userId),
|
|
405
|
+
getUserProfile(userId)
|
|
406
|
+
);
|
|
294
407
|
|
|
295
408
|
return success({ user, profile });
|
|
296
409
|
});
|
|
297
410
|
}
|
|
298
411
|
```
|
|
299
412
|
|
|
300
|
-
- if a yielded step fails,
|
|
413
|
+
- if a yielded step fails, that failure becomes the default unwind result
|
|
414
|
+
- cleanup still runs, and the last explicit `return` reached in `finally` wins
|
|
415
|
+
- yielded cleanup `Failure` values keep the current unwind result unless a
|
|
416
|
+
later cleanup `return` overrides it
|
|
301
417
|
- sync hydrated `Failable` helpers can use direct `yield* helper()` in both sync
|
|
302
418
|
and async builders
|
|
303
|
-
- promised sources
|
|
419
|
+
- promised sources in async builders use `yield* await promisedHelper()`
|
|
420
|
+
- in async builders, use `yield* await all(...)` to run multiple sources in
|
|
421
|
+
parallel and get a success tuple or the first failure
|
|
422
|
+
- use `yield* all(...)` in sync builders when every source is already a hydrated
|
|
423
|
+
`Failable`
|
|
424
|
+
- use `await allSettled(...)` to inspect the settled tuple of sources that
|
|
425
|
+
resolve to `Failable`; source promise rejections still reject unchanged
|
|
426
|
+
- use `yield* race(...)` when every raced source is already a hydrated
|
|
427
|
+
`Failable`
|
|
428
|
+
- use `yield* await race(...)` when any raced source is promised
|
|
429
|
+
- direct promised sources still follow normal async `await` / `try` /
|
|
430
|
+
`finally` semantics rather than a helper-managed rejection path
|
|
304
431
|
- `run(...)` does not capture thrown values or rejected promises into `Failure`;
|
|
305
432
|
wrap throwing boundaries with `failable(...)` before they enter `run(...)`
|
|
306
433
|
|
|
434
|
+
## Parallel Combinators
|
|
435
|
+
|
|
436
|
+
Import `all(...)`, `allSettled(...)`, and `race(...)` from the package root when
|
|
437
|
+
you want to combine multiple sources outside `run(...)` or inside async
|
|
438
|
+
builders.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import {
|
|
442
|
+
all,
|
|
443
|
+
allSettled,
|
|
444
|
+
failure,
|
|
445
|
+
race,
|
|
446
|
+
success,
|
|
447
|
+
} from '@pvorona/failable';
|
|
448
|
+
|
|
449
|
+
const syncTuple = all(success(1), success('two'));
|
|
450
|
+
const mixedTuple = await all(
|
|
451
|
+
success(1),
|
|
452
|
+
Promise.resolve(success('two'))
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const settled = await allSettled(
|
|
456
|
+
Promise.resolve(success(1)),
|
|
457
|
+
Promise.resolve(failure('missing-profile'))
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const syncWinner = race(
|
|
461
|
+
success('cached'),
|
|
462
|
+
success('stale')
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const mixedWinner = await race(
|
|
466
|
+
success('cached'),
|
|
467
|
+
Promise.resolve(success('network'))
|
|
468
|
+
);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Key semantics:
|
|
472
|
+
|
|
473
|
+
- `all(...)` returns the first failure in input order
|
|
474
|
+
- `allSettled(...)` returns a plain settled tuple rather than a `Success` wrapper
|
|
475
|
+
- `allSettled(...)` preserves `Failure` values in the returned settled tuple
|
|
476
|
+
- `allSettled(...)` only settles sources that resolve to `Failable` values
|
|
477
|
+
- promised source rejections in `allSettled(...)` still reject the combinator
|
|
478
|
+
- wrap rejecting boundaries with `failable(...)` first if you want a rejection
|
|
479
|
+
converted into `Failure`
|
|
480
|
+
- bare `Promise.reject(...)` inputs are rejected at type level as a best-effort
|
|
481
|
+
guardrail; TypeScript still cannot model arbitrary promise rejection channels
|
|
482
|
+
precisely
|
|
483
|
+
- `race(...)` accepts sync or promised `Failable` sources
|
|
484
|
+
- `race(...)` returns sync `Failable` when every source is sync, otherwise
|
|
485
|
+
`Promise<Failable>`
|
|
486
|
+
- when `race(...)` mixes already-settled sync and promised sources, winner
|
|
487
|
+
order follows normal `Promise.race(...)` input ordering
|
|
488
|
+
- `race()` with zero sources rejects with a clear error instead of hanging
|
|
489
|
+
|
|
307
490
|
## Transport And Runtime Validation
|
|
308
491
|
|
|
309
492
|
`Failable` values are hydrated objects with methods. Keep them inside your
|
|
@@ -318,7 +501,7 @@ import {
|
|
|
318
501
|
toFailableLike,
|
|
319
502
|
} from '@pvorona/failable';
|
|
320
503
|
|
|
321
|
-
const result = failure({ code: 'missing'
|
|
504
|
+
const result = failure({ code: 'missing' });
|
|
322
505
|
|
|
323
506
|
const wire = toFailableLike(result);
|
|
324
507
|
const hydrated = failable(wire);
|
|
@@ -348,14 +531,17 @@ if (isFailable(candidate) && candidate.isFailure) {
|
|
|
348
531
|
- `type Success<T>` / `type Failure<E>`: hydrated result variants
|
|
349
532
|
- `type FailableLike<T, E>`: structured-clone-friendly wire shape
|
|
350
533
|
- `success()` / `success(data)` / `failure()` / `failure(error)`: create hydrated results
|
|
351
|
-
- `
|
|
352
|
-
unchanged
|
|
534
|
+
- `throwIfFailure(result, normalizeOption?)` / `result.getOrThrow(normalizeOption?)`:
|
|
535
|
+
throw an `Error`, preserving existing `Error` instances unchanged by default
|
|
353
536
|
- `failable(...)`: preserve, rehydrate, capture, or normalize failures at
|
|
354
537
|
a boundary
|
|
355
538
|
- `run(...)`: compose `Failable` steps without nested branching
|
|
539
|
+
- `result.map(...)`: transform success data; failures pass through unchanged
|
|
540
|
+
- `result.flatMap(...)`: chain another `Failable`; failures short-circuit
|
|
356
541
|
- `toFailableLike(...)`: convert a hydrated result into a wire shape
|
|
357
542
|
- `isFailableLike(...)`: validate a wire shape
|
|
358
543
|
- `isFailable(...)`, `isSuccess(...)`, `isFailure(...)`: validate hydrated
|
|
359
544
|
results
|
|
360
|
-
- `NormalizedErrors`: built-in `Error` normalization for `failable(...)`
|
|
545
|
+
- `NormalizedErrors`: built-in `Error` normalization for `failable(...)` and
|
|
546
|
+
throw-boundary helpers
|
|
361
547
|
- `FailableStatus`: runtime success/failure status values
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { FailableStatus, NormalizedErrors, failable, failure, isFailable, isFailableLike, isFailure, isSuccess, run, success,
|
|
1
|
+
export { all, allSettled, FailableStatus, NormalizedErrors, failable, failure, isFailable, isFailableLike, isFailure, isSuccess, race, run, success, throwIfFailure, toFailableLike, } from './lib/failable.js';
|
|
2
2
|
export type { FailableNormalizeErrorOptions, Failable, FailableLike, FailableLikeFailure, FailableLikeSuccess, Failure, Success, } from './lib/failable.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,UAAU,EACV,cAAc,EACd,SAAS,EACT,SAAS,EACT,GAAG,EACH,OAAO,EACP,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,UAAU,EACV,cAAc,EACd,SAAS,EACT,SAAS,EACT,IAAI,EACJ,GAAG,EACH,OAAO,EACP,cAAc,EACd,cAAc,GACf,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,6BAA6B,EAC7B,QAAQ,EACR,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,OAAO,EACP,OAAO,GACR,MAAM,mBAAmB,CAAC"}
|