@owlmeans/error 0.1.18-rc.3 → 0.1.18-rc.30
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 +228 -39
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/error/SKILL.md +129 -20
- package/build/consts.d.ts +13 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +13 -0
- package/build/consts.js.map +1 -1
- package/build/i18n/fr.json +4 -0
- package/build/i18n.js +2 -0
- package/build/i18n.js.map +1 -1
- package/build/resilient.d.ts +21 -3
- package/build/resilient.d.ts.map +1 -1
- package/build/resilient.js +96 -13
- package/build/resilient.js.map +1 -1
- package/build/types.d.ts +6 -0
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +11 -7
- package/build/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/consts.ts +16 -0
- package/src/i18n/fr.json +4 -0
- package/src/i18n.ts +2 -0
- package/src/resilient.ts +111 -14
- package/src/types.ts +7 -0
- package/src/utils.ts +11 -9
- package/tests/copies.spec.ts +138 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +1 -1
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,78 +1,267 @@
|
|
|
1
1
|
# @owlmeans/error
|
|
2
2
|
|
|
3
|
-
Serializable error base class with type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Serializable error base class with a type registry, so a typed error thrown in one process is
|
|
4
|
+
caught as the same class in another. An app uses it for every error that has to cross a service
|
|
5
|
+
boundary (HTTP, socket, queue) or reach a UI as a translated message: declare a `ResilientError`
|
|
6
|
+
subclass, register it in the shared module both sides import, and normalize whatever you catch
|
|
7
|
+
with `ResilientError.ensure`. It is not for wiring faults — an unknown alias or a missing service
|
|
8
|
+
is a `SyntaxError` that must crash the process. Before declaring a new family, reuse the ones
|
|
9
|
+
framework packages already ship: authentication and authorization failures from
|
|
10
|
+
[`@owlmeans/auth`](../auth), storage failures from [`@owlmeans/resource`](../resource).
|
|
10
11
|
|
|
11
12
|
## Installation
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
bun add @owlmeans/error
|
|
15
|
+
bun add @owlmeans/error@^0.1.18-rc.30
|
|
15
16
|
```
|
|
16
17
|
|
|
18
|
+
## Concepts
|
|
19
|
+
|
|
20
|
+
- **Resilient error** — a `ResilientError` carries a `type` (the class's static `typeName`) next to
|
|
21
|
+
its `message`; the pair identifies the failure anywhere it travels.
|
|
22
|
+
- **Error family** — a base subclass that prefixes every message (`invoice:`) and subclasses that
|
|
23
|
+
add their own prefix and re-stamp `type`, so the final message reads `invoice:not-found:<id>`.
|
|
24
|
+
- **Registry** — `ResilientError.converters`, filled by `registerErrorClass`; only a registered
|
|
25
|
+
class comes back as itself after transport.
|
|
26
|
+
- **Marshalling** — `marshal` flattens `type`, `message` and the original stack into one plain
|
|
27
|
+
`Error` whose message is joined by `SEPARATOR` (`|||`); `ensure` recognises that prefix and
|
|
28
|
+
rebuilds the registered class.
|
|
29
|
+
- **Errors namespace** — importing the package registers the `errors` i18n library; UIs resolve a
|
|
30
|
+
message by the error's `type`, never by the thrown string.
|
|
31
|
+
|
|
17
32
|
## Usage
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
### Declare and register an error family
|
|
20
35
|
|
|
21
|
-
|
|
36
|
+
Keep errors in the shared (`common`) module that both the server and the client import, and
|
|
37
|
+
register every class at module load.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
22
40
|
import { ResilientError } from '@owlmeans/error'
|
|
23
41
|
|
|
24
|
-
export class
|
|
25
|
-
static typeName = '
|
|
42
|
+
export class InvoiceError extends ResilientError {
|
|
43
|
+
public static override typeName = 'MyAppInvoiceError'
|
|
44
|
+
|
|
45
|
+
constructor(message: string = 'error') {
|
|
46
|
+
super(InvoiceError.typeName, `invoice:${message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class InvoiceNotFound extends InvoiceError {
|
|
51
|
+
public static override typeName = `NotFound${InvoiceError.typeName}`
|
|
52
|
+
|
|
53
|
+
constructor(message: string = 'error') {
|
|
54
|
+
super(`not-found:${message}`)
|
|
55
|
+
this.type = InvoiceNotFound.typeName
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class InvoiceLocked extends InvoiceError {
|
|
60
|
+
public static override typeName = `Locked${InvoiceError.typeName}`
|
|
26
61
|
|
|
27
62
|
constructor(message: string = 'error') {
|
|
28
|
-
super(`
|
|
29
|
-
this.type =
|
|
63
|
+
super(`locked:${message}`)
|
|
64
|
+
this.type = InvoiceLocked.typeName
|
|
30
65
|
}
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
ResilientError.registerErrorClass(
|
|
68
|
+
ResilientError.registerErrorClass(InvoiceError)
|
|
69
|
+
ResilientError.registerErrorClass(InvoiceNotFound)
|
|
70
|
+
ResilientError.registerErrorClass(InvoiceLocked)
|
|
71
|
+
|
|
72
|
+
throw new InvoiceNotFound('inv-42') // message: 'invoice:not-found:inv-42'
|
|
34
73
|
```
|
|
35
74
|
|
|
36
|
-
|
|
75
|
+
A subclass of a subclass calls `super` with the message alone — the parent adds its own prefix and
|
|
76
|
+
`type`, so the subclass re-stamps `this.type` afterwards.
|
|
77
|
+
|
|
78
|
+
### Normalize what you caught
|
|
37
79
|
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
`ensure` accepts an `Error` or a string and always returns a `ResilientError`, so a handler can
|
|
81
|
+
branch on the registered classes it knows and fall through for the rest.
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
```ts
|
|
84
|
+
import { ResilientError } from '@owlmeans/error'
|
|
85
|
+
import { InvoiceLocked, InvoiceNotFound } from '@my-app/common'
|
|
43
86
|
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
export const settle = async (invoiceId: string) => {
|
|
88
|
+
try {
|
|
89
|
+
await payInvoice(invoiceId)
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const err = ResilientError.ensure(e as Error)
|
|
92
|
+
if (err instanceof InvoiceNotFound || err instanceof InvoiceLocked) {
|
|
93
|
+
return { settled: false, reason: err.type }
|
|
94
|
+
}
|
|
95
|
+
throw err
|
|
96
|
+
}
|
|
97
|
+
}
|
|
46
98
|
```
|
|
47
99
|
|
|
48
|
-
|
|
100
|
+
A small helper keeps catch blocks typed when the caught value is `unknown`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { ResilientError } from '@owlmeans/error'
|
|
104
|
+
|
|
105
|
+
export const asError = <T extends ResilientError>(err: unknown): T =>
|
|
106
|
+
typeof err === 'string' || err instanceof Error
|
|
107
|
+
? ResilientError.ensure(err) as T
|
|
108
|
+
: ResilientError.ensure(`Unknown error ${String(err)}`) as T
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cross a service boundary
|
|
112
|
+
|
|
113
|
+
The HTTP transports already do this: `@owlmeans/server-api` answers a thrown error with
|
|
114
|
+
`ResilientError.marshal(ResilientError.ensure(error)).message` as the body, and `@owlmeans/api`
|
|
115
|
+
turns a string body back into the registered class with `ResilientError.ensure`. So a backend throw
|
|
116
|
+
is caught as the same class around `context.entrypoint(invoiceProtocols.settle).call(...)` in the
|
|
117
|
+
client. A boundary you own — a WebSocket close reason, a stored failure record — does the same by hand:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { enuserError, marshalError } from '@owlmeans/error'
|
|
121
|
+
import { InvoiceError } from '@my-app/common'
|
|
122
|
+
|
|
123
|
+
// Producer: send only the flattened string.
|
|
124
|
+
socket.close(1011, marshalError(caught).message)
|
|
125
|
+
|
|
126
|
+
// Consumer: rebuild the registered class, typed to the family you expect.
|
|
127
|
+
socket.addEventListener('close', event => {
|
|
128
|
+
const err = enuserError<InvoiceError>(event.reason)
|
|
129
|
+
if (err instanceof InvoiceError) {
|
|
130
|
+
showInvoiceProblem(err.type)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The round trip is worth a test for any error whose identity the far side depends on:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { expect, test } from 'bun:test'
|
|
139
|
+
import { ResilientError } from '@owlmeans/error'
|
|
140
|
+
import { InvoiceNotFound } from '@my-app/common'
|
|
141
|
+
|
|
142
|
+
test('an invoice miss survives the hop as its own class', () => {
|
|
143
|
+
const restored = ResilientError.ensure(
|
|
144
|
+
ResilientError.marshal(new InvoiceNotFound('inv-42')).message
|
|
145
|
+
)
|
|
49
146
|
|
|
50
|
-
|
|
147
|
+
expect(restored).toBeInstanceOf(InvoiceNotFound)
|
|
148
|
+
expect(restored.message).toBe('invoice:not-found:inv-42')
|
|
149
|
+
})
|
|
150
|
+
```
|
|
51
151
|
|
|
52
|
-
|
|
152
|
+
### Rebuild state after unmarshalling
|
|
53
153
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- `static ensure(err, throwOnUnknown?)` — convert any error to `ResilientError`
|
|
57
|
-
- `static marshal(err)` — serialize an error to a transportable `Error` object
|
|
58
|
-
- `type: string` — error type identifier set in the constructor
|
|
154
|
+
Only `type`, `message` and the stack travel. A subclass that exposes structured data derives it
|
|
155
|
+
from the message in `finalizeUnmarshal()`, which runs after the message is restored.
|
|
59
156
|
|
|
60
|
-
|
|
157
|
+
```ts
|
|
158
|
+
import { ResilientError } from '@owlmeans/error'
|
|
61
159
|
|
|
62
|
-
|
|
160
|
+
export class QuotaExceeded extends ResilientError {
|
|
161
|
+
public static override typeName = 'MyAppQuotaExceeded'
|
|
162
|
+
|
|
163
|
+
public limit?: number
|
|
164
|
+
|
|
165
|
+
constructor(message: string = 'error') {
|
|
166
|
+
super(QuotaExceeded.typeName, `quota:${message}`)
|
|
167
|
+
this.finalizeUnmarshal()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override finalizeUnmarshal(): void {
|
|
171
|
+
const tail = Number(this.message.split(':').pop())
|
|
172
|
+
this.limit = Number.isFinite(tail) ? tail : undefined
|
|
173
|
+
}
|
|
174
|
+
}
|
|
63
175
|
|
|
64
|
-
|
|
176
|
+
ResilientError.registerErrorClass(QuotaExceeded)
|
|
177
|
+
```
|
|
65
178
|
|
|
66
|
-
|
|
179
|
+
### Translate errors in the UI
|
|
67
180
|
|
|
68
|
-
|
|
181
|
+
Panel components resolve an error through `errors.<type>` — a form- or screen-scoped
|
|
182
|
+
`<name>.errors.<type>` key first, then `errors.<type>` in the screen's namespace, then the shared
|
|
183
|
+
`errors` library. Ship a translation for every type you declare, in every supported language:
|
|
69
184
|
|
|
70
|
-
```
|
|
71
|
-
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"errors": {
|
|
188
|
+
"MyAppInvoiceError": "The invoice could not be processed",
|
|
189
|
+
"NotFoundMyAppInvoiceError": "This invoice no longer exists",
|
|
190
|
+
"LockedMyAppInvoiceError": "The invoice is being settled, try again shortly"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
72
193
|
```
|
|
73
194
|
|
|
74
|
-
##
|
|
195
|
+
## API
|
|
196
|
+
|
|
197
|
+
### Classes
|
|
198
|
+
|
|
199
|
+
| Symbol | Kind | Purpose |
|
|
200
|
+
|--------|------|---------|
|
|
201
|
+
| `ResilientError` | class | Base class of every framework error; `constructor(type, message, stack?)` |
|
|
202
|
+
| `ResilientError.typeName` | static property | Type identifier; override in each subclass (`'ResilientError'` on the base) |
|
|
203
|
+
| `ResilientError.separator` | static property | Marshalling separator, initialised from `SEPARATOR` |
|
|
204
|
+
| `ResilientError.converters` | static property | The registry of `Converter` entries — one array on `globalThis`, shared by every copy of the package in the process |
|
|
205
|
+
| `ResilientError[Symbol.hasInstance]` | static method | `instanceof` that also matches an instance of the same class lineage built by another copy of the package |
|
|
206
|
+
| `ResilientError.registerErrorClass(Class, errorClass?)` | static method | Registers a subclass so it survives a round trip; returns its `Converter` |
|
|
207
|
+
| `ResilientError.ensure(err, throwOnUnknown?)` | static method | Turns an `Error` or string into a `ResilientError`, unmarshalling registered classes |
|
|
208
|
+
| `ResilientError.marshal(err)` | static method | Flattens an error into a plain `Error` whose message is `type`, `message` and stack joined by `SEPARATOR` |
|
|
209
|
+
| `error.type` | instance property | The error's type identifier |
|
|
210
|
+
| `error.oiriginalStack` | instance property | The stack captured at the original throw (spelling as in source) |
|
|
211
|
+
| `error.marshal()` | instance method | `ResilientError.marshal(this)` |
|
|
212
|
+
| `error.finalizeUnmarshal()` | instance method | Hook run after unmarshalling; no-op by default |
|
|
213
|
+
|
|
214
|
+
### Functions and constants
|
|
215
|
+
|
|
216
|
+
| Symbol | Kind | Purpose |
|
|
217
|
+
|--------|------|---------|
|
|
218
|
+
| `enuserError<T>(err, throwOnUnknown?)` | function | `ResilientError.ensure`, typed to the subclass you expect |
|
|
219
|
+
| `marshalError(err)` | function | `ensure` then `marshal`, for a boundary that only carries an `Error` or a string |
|
|
220
|
+
| `isResilientError(value)` | function | Structural check (shared brand + `type` + `marshal`) that holds across duplicate module copies |
|
|
221
|
+
| `SEPARATOR` | constant | Three pipe characters — joins the marshalled fields |
|
|
222
|
+
| `RESILENT_ERROR` | constant | `'ResilientError'` — the base type name |
|
|
223
|
+
| `RESILIENT_BRAND`, `CONVERTER_REGISTRY`, `CATCH_ALL_CONVERTER` | constant | `Symbol.for` keys every copy of the package shares |
|
|
224
|
+
|
|
225
|
+
### Types
|
|
226
|
+
|
|
227
|
+
| Symbol | Kind | Purpose |
|
|
228
|
+
|--------|------|---------|
|
|
229
|
+
| `ValueOrError<T>` | type | `T \| ResilientError`, for a result that carries either |
|
|
230
|
+
| `Converter` | interface | `{ match, convert, isMarshaled, unmarshal }` — one registry entry |
|
|
231
|
+
| `ResilientErrorConstructor<T>` | interface | The constructor shape `registerErrorClass` accepts: `new (message, stack?)` plus `typeName` |
|
|
232
|
+
|
|
233
|
+
### Side effect
|
|
234
|
+
|
|
235
|
+
Importing the package registers the `errors` i18n library (via `addI18nLib` from
|
|
236
|
+
`@owlmeans/i18n`) for `en`, `pl`, `ru`, `be`, `uk`, `es` and `de`. There are no subpath exports.
|
|
237
|
+
|
|
238
|
+
## Common pitfalls
|
|
239
|
+
|
|
240
|
+
- Register every subclass. An unregistered error comes back as a bare `ResilientError` whose
|
|
241
|
+
`type` is the whole `Type|||message|||stack` string.
|
|
242
|
+
- `ensure` on an unregistered, unmarshalled throw shifts the fields: `new Error('boom')` arrives
|
|
243
|
+
with `type: 'boom'` and the stack as its `message`. Read `.type` only on errors that came back
|
|
244
|
+
through the registered path.
|
|
245
|
+
- `SyntaxError` is rethrown by `ensure`, never converted. Do not throw one for a runtime condition a
|
|
246
|
+
caller is expected to handle.
|
|
247
|
+
- The second argument of `registerErrorClass` and `ensure`'s `throwOnUnknown` have no effect — a
|
|
248
|
+
catch-all converter is matched first. Register the class alone and treat `ensure` as one-argument.
|
|
249
|
+
- A subclass constructor must accept `(message, stack?)`: unmarshalling calls `new Class(message,
|
|
250
|
+
stack)`. Keep extra state in the message and rebuild it in `finalizeUnmarshal()`.
|
|
251
|
+
- Registration happens at import time. The module declaring the errors must be loaded on the
|
|
252
|
+
receiving side before an error is ensured, or the class cannot be rebuilt.
|
|
253
|
+
- Keep `typeName` unique per process; show users the translation of `errors.<type>`, never the
|
|
254
|
+
thrown message.
|
|
255
|
+
|
|
256
|
+
## Related packages
|
|
75
257
|
|
|
258
|
+
- [`@owlmeans/i18n`](../i18n) — the registry the `errors` library is added to
|
|
259
|
+
- [`@owlmeans/client-i18n`](../client-i18n) — React hooks that resolve `errors.<type>` keys
|
|
260
|
+
- [`@owlmeans/auth`](../auth) — the authentication and authorization error hierarchy
|
|
261
|
+
- [`@owlmeans/resource`](../resource) — resource errors built on `ResilientError`
|
|
262
|
+
- [`@owlmeans/server-api`](../server-api) — marshals thrown errors into HTTP responses
|
|
263
|
+
- [`@owlmeans/api`](../api) — rebuilds marshalled errors from HTTP responses
|
|
264
|
+
- [`@owlmeans/client-panel`](../client-panel) — panel and form components that translate errors by type
|
|
76
265
|
- [`@owlmeans/context`](../context) — context and services that propagate errors through the app
|
|
77
266
|
|
|
78
267
|
<!-- owlmeans:agent-guidance:start -->
|
|
@@ -83,7 +272,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
83
272
|
your project's skill store (`.agents/skills/`):
|
|
84
273
|
|
|
85
274
|
```sh
|
|
86
|
-
npx @owlmeans/agent-skills
|
|
275
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.32
|
|
87
276
|
```
|
|
88
277
|
|
|
89
278
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/error",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.30",
|
|
5
|
+
"generatedAt": "2026-09-22T12:36:21.867Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: error
|
|
3
|
-
description: How to use @owlmeans/error —
|
|
3
|
+
description: How to use @owlmeans/error — ResilientError, the error class registry, marshalling errors across a service boundary and back, and the i18n namespace error messages resolve through. Auto-invoked when importing from this package, declaring a typed framework error, or normalizing a caught error.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,44 +8,153 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/error
|
|
9
9
|
|
|
10
10
|
**Layer:** Core
|
|
11
|
-
**Install:** `"@owlmeans/error": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/error": "^0.1.18-rc.30"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
15
15
|
| Export | Description |
|
|
16
16
|
|--------|-------------|
|
|
17
|
-
| `ResilientError` | Base
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
|
|
|
17
|
+
| `ResilientError` | Base class — every framework error extends it |
|
|
18
|
+
| `ResilientError.registerErrorClass(Class)` | Register a subclass so it survives a round trip |
|
|
19
|
+
| `ResilientError.ensure(err)` | Turn anything caught into a `ResilientError` |
|
|
20
|
+
| `ResilientError.marshal(err)` / `err.marshal()` | Flatten into a plain `Error` a transport can carry |
|
|
21
|
+
| `enuserError(err)` | `ensure`, typed to the subclass you expect |
|
|
22
|
+
| `marshalError(err)` | `ensure` then `marshal`, for a boundary that only sends `Error` |
|
|
23
|
+
| `isResilientError(value)` | Structural check that holds across duplicate module copies |
|
|
24
|
+
| `SEPARATOR` (`'\|\|\|'`), `RESILENT_ERROR` | The marshalling separator and the base type name |
|
|
25
|
+
| `RESILIENT_BRAND`, `CONVERTER_REGISTRY`, `CATCH_ALL_CONVERTER` | The `Symbol.for` keys every copy shares |
|
|
26
|
+
| `Converter` | `{ match, convert, isMarshaled, unmarshal }` — one registry entry |
|
|
27
|
+
| `ResilientErrorConstructor` | The constructor shape `registerErrorClass` accepts |
|
|
28
|
+
| `ValueOrError<T>` | `T \| ResilientError`, for a result that carries either |
|
|
21
29
|
|
|
22
|
-
##
|
|
30
|
+
## Declaring an error
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
A subclass owns a static `typeName` and prefixes its messages, so the pair `type` + `message` is
|
|
33
|
+
enough to identify what went wrong anywhere the error travels. **Register it** — registration is
|
|
34
|
+
what makes a marshaled error come back as the class it was thrown as. Skip it and the far side gets
|
|
35
|
+
an unusable `ResilientError` whose `type` is the whole marshaled string (see below).
|
|
25
36
|
|
|
26
37
|
```typescript
|
|
27
38
|
import { ResilientError } from '@owlmeans/error'
|
|
28
39
|
|
|
29
|
-
export class
|
|
30
|
-
public static override typeName = '
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
export class ApiError extends ResilientError {
|
|
41
|
+
public static override typeName = 'ApiError'
|
|
42
|
+
|
|
43
|
+
constructor(message: string = 'error') {
|
|
44
|
+
super(ApiError.typeName, `api:${message}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RateLimitError extends ApiError {
|
|
49
|
+
public static override typeName = `${ApiError.typeName}:RateLimit`
|
|
50
|
+
|
|
51
|
+
constructor(message: string = 'error') {
|
|
52
|
+
super(`rate-limit:${message}`)
|
|
53
|
+
this.type = RateLimitError.typeName
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
ResilientError.registerErrorClass(ApiError)
|
|
58
|
+
ResilientError.registerErrorClass(RateLimitError)
|
|
59
|
+
|
|
60
|
+
throw new RateLimitError('per-minute')
|
|
38
61
|
```
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
A subclass of a subclass calls `super` with the message alone and then re-stamps `this.type` — the
|
|
64
|
+
parent supplies its own prefix, so the final message reads `api:rate-limit:per-minute`.
|
|
65
|
+
|
|
66
|
+
**A refusal of the caller's condition declares its HTTP status** as `public static httpStatus = <4xx>`;
|
|
67
|
+
a fault — a broken peer, a timeout, a missing configuration, a bug — declares nothing and answers 500.
|
|
68
|
+
One principle picks the number:
|
|
69
|
+
|
|
70
|
+
| Status | The caller's condition |
|
|
71
|
+
|---|---|
|
|
72
|
+
| 400 | the request is malformed |
|
|
73
|
+
| 402 | the balance or plan does not pay for it |
|
|
74
|
+
| 404 | the addressed target does not exist, or is another organization's |
|
|
75
|
+
| 409 | the target's current state conflicts with the request |
|
|
76
|
+
| 422 | the request is understood, and its content or body is refused |
|
|
77
|
+
|
|
78
|
+
Declare it on the leaf class, never on a family base: a static is inherited, so a base's status
|
|
79
|
+
would reach every fault that extends it. A class thrown for several causes takes the status of the
|
|
80
|
+
cause a caller can reach, and one whose causes are told apart only by message declares nothing.
|
|
81
|
+
Nothing here reads it — `@owlmeans/server-api` does, structurally, and answers 500 for any class
|
|
82
|
+
without an integer 4xx declaration (see the `server-api` skill). A permission refusal extends
|
|
83
|
+
`AuthForbidden` (403) instead of declaring.
|
|
84
|
+
|
|
85
|
+
`registerErrorClass` takes a second, native-class argument, and `ensure` takes a second
|
|
86
|
+
`throwOnUnknown` argument. **Neither has any effect** — a catch-all converter is pushed onto the
|
|
87
|
+
registry when this package loads and it is the first entry `ensure` tests for conversion, so no
|
|
88
|
+
later converter and no `throwOnUnknown` branch is ever reached. Register the class alone, and treat
|
|
89
|
+
`ensure` as taking one argument.
|
|
90
|
+
|
|
91
|
+
## Normalizing what you caught
|
|
92
|
+
|
|
93
|
+
`ensure` gives you a `ResilientError` for anything caught, but only a **registered, marshaled**
|
|
94
|
+
error survives with its identity intact. Route errors that must keep their type through
|
|
95
|
+
`marshal`/`ensure`; do not rely on `ensure` alone to normalize an arbitrary throw.
|
|
96
|
+
|
|
41
97
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// err is now a ResilientError with stable .type and .message
|
|
98
|
+
try { /* ... */ } catch (e) {
|
|
99
|
+
const err = ResilientError.ensure(e as Error)
|
|
100
|
+
if (err instanceof RateLimitError) { /* the registered class came back */ }
|
|
46
101
|
}
|
|
47
102
|
```
|
|
48
103
|
|
|
104
|
+
What `ensure` actually does, in order:
|
|
105
|
+
|
|
106
|
+
| Input | Result |
|
|
107
|
+
|-------|--------|
|
|
108
|
+
| A `ResilientError` from any copy of this package | returned untouched |
|
|
109
|
+
| A `SyntaxError` | rethrown — never converted |
|
|
110
|
+
| An `Error` marshaled from a **registered** class | unmarshaled into that class, `type` and `message` restored |
|
|
111
|
+
| Anything else | a bare `ResilientError` whose **`type` is the original `message`** and whose **`message` is the original stack** |
|
|
112
|
+
|
|
113
|
+
That last row is the trap: the fields are shifted, so an unregistered marshaled error arrives with
|
|
114
|
+
`type` set to the whole `Type|||message|||stack` string, and a plain `new Error('boom')` arrives with
|
|
115
|
+
`type: 'boom'`. Read `.type` only where the error came back through the registered path; keep the
|
|
116
|
+
original around when you need its message.
|
|
117
|
+
|
|
118
|
+
**`SyntaxError` is never converted — it is rethrown.** A `SyntaxError` in this framework means the
|
|
119
|
+
process is wired wrong (an unknown alias, a missing service, a route cycle), and it must crash
|
|
120
|
+
rather than reach a user as a handled failure. Do not throw one for a runtime condition a caller is
|
|
121
|
+
expected to handle.
|
|
122
|
+
|
|
123
|
+
## Duplicate module copies behave as one
|
|
124
|
+
|
|
125
|
+
A process can load this package more than once — `bun --preserve-symlinks` over linked workspaces
|
|
126
|
+
keys a module by its unresolved path, so a package that keeps its own
|
|
127
|
+
`node_modules/@owlmeans/error` link loads a second copy. Every copy acts as one:
|
|
128
|
+
|
|
129
|
+
- **One registry.** `ResilientError.converters` is the array kept on `globalThis` under
|
|
130
|
+
`Symbol.for('@owlmeans/error:converters')`; every copy registers into it and unmarshals from it,
|
|
131
|
+
in registration order, so the **last registration of a type name wins** across copies. One
|
|
132
|
+
catch-all converter exists per process.
|
|
133
|
+
- **One brand.** Every instance inherits `Symbol.for('@owlmeans/error:resilient')` from its copy's
|
|
134
|
+
prototype. `isResilientError(value)` is brand + string `type` + `marshal` function; `ensure`
|
|
135
|
+
returns such a value untouched and `marshal` keeps its `type`. A lookalike without the brand is
|
|
136
|
+
not a resilient error.
|
|
137
|
+
- **`instanceof` across copies.** `ResilientError[Symbol.hasInstance]` answers natively first, then
|
|
138
|
+
structurally: the instance's lineage of OWN `typeName`s must end with the checked class's lineage.
|
|
139
|
+
A class from another copy matches itself and its ancestors, never a sibling or a parent. A class
|
|
140
|
+
that does not declare its own `typeName` matches natively only — declare one on every subclass.
|
|
141
|
+
|
|
142
|
+
## Crossing a service boundary
|
|
143
|
+
|
|
144
|
+
`marshal` flattens `type`, `message`, the selected stack exposure and an optional incident id into
|
|
145
|
+
one `Error` message joined by `SEPARATOR`. On the far side `ensure` recognises the prefix and rebuilds
|
|
146
|
+
the registered class, so a typed error thrown in a backend is caught as the same class in a client.
|
|
147
|
+
The HTTP boundary attaches the incident id to the logged error and sends only that id in production;
|
|
148
|
+
development callers may select the stack explicitly. Override
|
|
149
|
+
`finalizeUnmarshal()` on a subclass that needs to rebuild state from its message after that.
|
|
150
|
+
|
|
151
|
+
## Messages are i18n keys
|
|
152
|
+
|
|
153
|
+
Importing this package registers the `errors` translation library for every bundled locale. UIs
|
|
154
|
+
resolve an error by its `type` — `errors.<type>`, with a form- or screen-scoped key tried first —
|
|
155
|
+
so the message a user reads comes from the translations, never from the thrown string. Ship a
|
|
156
|
+
translation for each error type you declare.
|
|
157
|
+
|
|
49
158
|
## Depends On
|
|
50
159
|
|
|
51
|
-
- `@owlmeans/i18n` —
|
|
160
|
+
- `@owlmeans/i18n` — the translation library the `errors` namespace is registered in
|
package/build/consts.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
export declare const SEPARATOR = "|||";
|
|
2
2
|
export declare const RESILENT_ERROR = "ResilientError";
|
|
3
|
+
/**
|
|
4
|
+
* Marks every `ResilientError` instance, whichever copy of this module built it.
|
|
5
|
+
*
|
|
6
|
+
* A process can hold several copies of this package (`bun --preserve-symlinks` over a linked
|
|
7
|
+
* workspace resolves one package through several `node_modules` paths), and `instanceof` answers
|
|
8
|
+
* for one copy only. `Symbol.for` returns the same symbol in every copy, so the brand is the
|
|
9
|
+
* structural proof `ensure`, `marshal` and `instanceof` read.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RESILIENT_BRAND: unique symbol;
|
|
12
|
+
/** The `globalThis` key of the converter registry every copy of this module shares. */
|
|
13
|
+
export declare const CONVERTER_REGISTRY: unique symbol;
|
|
14
|
+
/** Marks the catch-all converter, so a second module copy does not push another one. */
|
|
15
|
+
export declare const CATCH_ALL_CONVERTER: unique symbol;
|
|
3
16
|
//# sourceMappingURL=consts.d.ts.map
|
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,SAAS,QAAQ,CAAA;AAE9B,eAAO,MAAM,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,SAAS,QAAQ,CAAA;AAE9B,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,MAAgD,CAAA;AAErF,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,EAAE,OAAO,MAAiD,CAAA;AAEzF,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAgD,CAAA"}
|
package/build/consts.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
export const SEPARATOR = '|||';
|
|
2
2
|
export const RESILENT_ERROR = 'ResilientError';
|
|
3
|
+
/**
|
|
4
|
+
* Marks every `ResilientError` instance, whichever copy of this module built it.
|
|
5
|
+
*
|
|
6
|
+
* A process can hold several copies of this package (`bun --preserve-symlinks` over a linked
|
|
7
|
+
* workspace resolves one package through several `node_modules` paths), and `instanceof` answers
|
|
8
|
+
* for one copy only. `Symbol.for` returns the same symbol in every copy, so the brand is the
|
|
9
|
+
* structural proof `ensure`, `marshal` and `instanceof` read.
|
|
10
|
+
*/
|
|
11
|
+
export const RESILIENT_BRAND = Symbol.for('@owlmeans/error:resilient');
|
|
12
|
+
/** The `globalThis` key of the converter registry every copy of this module shares. */
|
|
13
|
+
export const CONVERTER_REGISTRY = Symbol.for('@owlmeans/error:converters');
|
|
14
|
+
/** Marks the catch-all converter, so a second module copy does not push another one. */
|
|
15
|
+
export const CATCH_ALL_CONVERTER = Symbol.for('@owlmeans/error:catch-all');
|
|
3
16
|
//# sourceMappingURL=consts.js.map
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAA;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAA;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA;AAE9C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAErF,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAkB,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;AAEzF,wFAAwF;AACxF,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA"}
|
package/build/i18n.js
CHANGED
|
@@ -6,6 +6,7 @@ import be from './i18n/be.json' with { type: 'json' };
|
|
|
6
6
|
import uk from './i18n/uk.json' with { type: 'json' };
|
|
7
7
|
import es from './i18n/es.json' with { type: 'json' };
|
|
8
8
|
import de from './i18n/de.json' with { type: 'json' };
|
|
9
|
+
import fr from './i18n/fr.json' with { type: 'json' };
|
|
9
10
|
addI18nLib('en', 'errors', en);
|
|
10
11
|
addI18nLib('pl', 'errors', pl);
|
|
11
12
|
addI18nLib('ru', 'errors', ru);
|
|
@@ -13,4 +14,5 @@ addI18nLib('be', 'errors', be);
|
|
|
13
14
|
addI18nLib('uk', 'errors', uk);
|
|
14
15
|
addI18nLib('es', 'errors', es);
|
|
15
16
|
addI18nLib('de', 'errors', de);
|
|
17
|
+
addI18nLib('fr', 'errors', fr);
|
|
16
18
|
//# sourceMappingURL=i18n.js.map
|
package/build/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE3C,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAErD,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE3C,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAErD,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC9B,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA"}
|
package/build/resilient.d.ts
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import type { Converter, ResilientErrorConstructor } from './types.js';
|
|
1
|
+
import type { Converter, MarshalErrorOptions, ResilientErrorConstructor } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a value is a `ResilientError` built by ANY copy of this module.
|
|
4
|
+
*
|
|
5
|
+
* Structural: the shared brand plus the `type` and `marshal` every instance carries.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isResilientError: (value: unknown) => value is ResilientError;
|
|
2
8
|
export declare class ResilientError extends Error {
|
|
3
9
|
static separator: string;
|
|
4
10
|
static typeName: string;
|
|
5
11
|
static converters: Converter[];
|
|
12
|
+
/**
|
|
13
|
+
* `instanceof` that holds across module copies.
|
|
14
|
+
*
|
|
15
|
+
* The native prototype check answers first. Failing that, a branded instance matches when its
|
|
16
|
+
* class lineage ENDS WITH this class's lineage — the same own type names, in the same order, up
|
|
17
|
+
* to the base. So `instanceof AuthFailedError` accepts an `AuthFailedError` (or a subclass of it)
|
|
18
|
+
* from another copy, and never a sibling class or a parent. A class that does not declare its
|
|
19
|
+
* own `typeName` cannot be told apart structurally and answers natively only.
|
|
20
|
+
*/
|
|
21
|
+
static [Symbol.hasInstance]<T extends abstract new (...args: any[]) => any>(this: T, instance: unknown): instance is InstanceType<T>;
|
|
6
22
|
static registerErrorClass(resilientErrorClass: ResilientErrorConstructor, errorClass?: ErrorConstructor): Converter;
|
|
7
23
|
static ensure(err: Error | string, throwOnUnknown?: boolean): ResilientError;
|
|
8
|
-
static marshal(err: Error): Error;
|
|
24
|
+
static marshal(err: Error, opts?: MarshalErrorOptions): Error;
|
|
9
25
|
type: string;
|
|
10
26
|
oiriginalStack?: string;
|
|
27
|
+
/** Correlates a serialized boundary error with the server log entry that owns its full stack. */
|
|
28
|
+
incidentId?: string;
|
|
11
29
|
constructor(type: string, message: string, stack?: string);
|
|
12
|
-
marshal(): Error;
|
|
30
|
+
marshal(opts?: MarshalErrorOptions): Error;
|
|
13
31
|
finalizeUnmarshal(): void;
|
|
14
32
|
}
|
|
15
33
|
//# sourceMappingURL=resilient.d.ts.map
|
package/build/resilient.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resilient.d.ts","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"resilient.d.ts","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAyC3F;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,UAAW,OAAO,KAAG,KAAK,IAAI,cAS1D,CAAA;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,OAAc,SAAS,EAAE,MAAM,CAAY;IAE3C,OAAc,QAAQ,EAAE,MAAM,CAAiB;IAE/C,OAAc,UAAU,EAAE,SAAS,EAAE,CAAqB;IAE1D;;;;;;;;OAQG;IACH,OAAuB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxF,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,GACzB,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,CAY7B;IAED,OAAc,kBAAkB,CAAC,mBAAmB,EAAE,yBAAyB,EAAE,UAAU,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAKzH;IAED,OAAc,MAAM,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,cAAc,CA4BlF;IAED,OAAc,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAenE;IAEM,IAAI,EAAE,MAAM,CAAiB;IAE7B,cAAc,CAAC,EAAE,MAAM,CAAA;IAE9B,iGAAiG;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAA;IAE1B,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAQxD;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAEzC;IAED,iBAAiB,IAAI,IAAI,CAAI;CAC9B"}
|