@odg/chemical-x 2.1.3 → 2.2.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 CHANGED
@@ -27,6 +27,8 @@
27
27
 
28
28
  # Table of Contents
29
29
 
30
+ > See [agents.md](agents.md) for consumer API reference. Detailed guides in [docs/](docs/).
31
+
30
32
  - [🎇 Benefits](#-benefits)
31
33
  - [📗 Libraries](#-libraries)
32
34
  - [📁 Dependencies](#-dependencies)
package/agents.md ADDED
@@ -0,0 +1,157 @@
1
+ ## @odg/chemical-x - Consumer Guide
2
+
3
+ ## 🎯 Purpose
4
+
5
+ - Framework TypeScript para automação web (scraping, crawling) com abstração sobre Puppeteer/Playwright, retry com lifecycle hooks e DSL baseada em decorators.
6
+ - Helpers utilitários (`retry`, `sleep`, `timeout`, `throwIf`) para controle de fluxo assíncrono com tratamento de erros tipado.
7
+ - Arquitetura Page/Handler: Pages executam ações por **intenção** (não por URL); Handlers validam transições e declaram soluções ou exceções.
8
+
9
+ ## 🚀 Quick Start
10
+
11
+ ```typescript
12
+ // Helpers — sem browser driver necessário
13
+ import { retry, sleep, timeout, throwIf, RetryAction } from "@odg/chemical-x";
14
+
15
+ const result = await retry({
16
+ times: 3,
17
+ sleep: 1000,
18
+ callback: async (attempt) => { /* operação retriable */ },
19
+ });
20
+
21
+ // Crawler — requer Puppeteer ou Playwright instalado
22
+ import { BrowserManager, BasePage, BaseHandler, Container } from "@odg/chemical-x";
23
+ ```
24
+
25
+ ## 📜 Quick API Reference
26
+
27
+ **Helpers** — Controle de fluxo assíncrono:
28
+
29
+ | Função | Propósito |
30
+ |---|---|
31
+ | `retry(options)` | Retenta callback N vezes com sleep, abort signal e callback `when` para decidir ação por tentativa |
32
+ | `sleep(ms, options?)` | Pausa assíncrona com suporte a `AbortSignal` |
33
+ | `timeout(options)` | Envolve callback com limite de tempo; lança `TimeoutException` se exceder |
34
+ | `throwIf(condition, exception)` | Lança exceção condicionalmente; tipagem `never` quando `condition: true` |
35
+
36
+ 📖 See also: [docs/helpers.md](docs/helpers.md)
37
+
38
+ **Decorators** — DSL para classes:
39
+
40
+ | Decorator | Propósito |
41
+ |---|---|
42
+ | `@attemptableFlow()` | Retry a nível de classe com lifecycle hooks (`attempt`, `sleep`, `success`, `failure`, `retrying`, `finish`) |
43
+ | `@getterAccess()` | Proxy que intercepta todo acesso a propriedades via `__get(key, value)` |
44
+ | `@injectablePageOrHandler(name)` | Registra classe no Container (Inversify) com `@injectable` + `@injectFromHierarchy` |
45
+ | `@registerListener(event, container, options)` | Registra listener de eventos em Container EventEmitter |
46
+
47
+ 📖 See also: [docs/decorators.md](docs/decorators.md)
48
+
49
+ **Crawler** — Automação web:
50
+
51
+ | Classe | Propósito |
52
+ |---|---|
53
+ | `BrowserManager` | Orquestrador: cria instâncias de Browser e Context via factories injetadas |
54
+ | `Browser` | Wrapper com `@getterAccess` sobre engine do browser; gerencia Contexts |
55
+ | `Context` | Wrapper sobre contexto do browser; gerencia Pages |
56
+ | `Page` | Wrapper sobre page do browser com acesso ao Context pai |
57
+ | `BasePage` (abstract) | Define uma página por intenção; implementa `execute()` + `attempt()` + seletores `$s`/`$$s` |
58
+ | `BaseHandler` (abstract) | Valida transições; implementa `waitForHandler()` + `attempt()`; declara Solution ou Exception |
59
+
60
+ 📖 See also: [docs/crawlers.md](docs/crawlers.md)
61
+
62
+ **Support** — Utilidades tipadas:
63
+
64
+ | Classe | Propósito |
65
+ |---|---|
66
+ | `Str` | Manipulação de string: extração monetária (`money()`, `moneys()`), `onlyNumbers()`, `ucFirst()`, `isJson()`, `formatUnicorn()` |
67
+ | `Num` | Wrapper numérico com `toNative()` e `clone()` |
68
+ | `Arr<Type>` | Wrapper de array com `random(length?)` e `clone()` |
69
+ | `File` | Verificação de existência de arquivo via `exists()` |
70
+
71
+ **Enums:**
72
+
73
+ | Enum | Valores |
74
+ |---|---|
75
+ | `RetryAction` | `Retry` (forçar retry), `Throw` (lançar), `Resolve` (resolver com undefined), `Default` (seguir `times`) |
76
+
77
+ **Container:**
78
+
79
+ - `Container<T>` estende `TypedContainer` (Inversify); adiciona `getOptional(name)` que retorna `undefined` se não registrado.
80
+
81
+ ## 🚦 Key Rules
82
+
83
+ 1. **`@attemptableFlow` vs `retry()`**: Use `@attemptableFlow` para retry a nível de classe com lifecycle hooks completo (attempt, success, failure, retrying, finish, sleep). Use `retry()` para retentativa simples de um callback isolado.
84
+ 📖 See also: [docs/decorators.md](docs/decorators.md)
85
+
86
+ 2. **`@getterAccess` — Proxy total**: Todo acesso a propriedade/método passa por `__get(key, value)`. Implementações de Browser, Context e Page usam isso para delegar ao engine subjacente.
87
+ 📖 See also: [docs/decorators.md](docs/decorators.md)
88
+
89
+ 3. **Page Intent Design**: Pages agrupam por **intenção**, não por URL. Uma mesma URL pode ter múltiplas Pages (ex: `LoginPage` para autenticar, `HomeVerificationPage` para validar conteúdo).
90
+ 📖 See also: [docs/crawlers.md](docs/crawlers.md)
91
+
92
+ 4. **Handler Validation Contract**: Handlers **validam**, não executam. `waitForHandler()` retorna `Exception` ou `() => Promise<HandlerSolutionType>`. O handler declara Solution (próxima Page) ou lança Exception. Nunca falha silenciosamente.
93
+ 📖 See also: [docs/crawlers.md](docs/crawlers.md)
94
+
95
+ 5. **Container.loadModule() obrigatório**: Classes com `@injectablePageOrHandler` precisam de `Container.loadModule()` antes da execução. DI binding é responsabilidade do consumidor.
96
+ 📖 See also: [docs/crawlers.md](docs/crawlers.md)
97
+
98
+ 6. **`retry()` com `when` callback**: O callback `when(exception, times)` retorna `RetryAction` para decidir por tentativa: `Retry` (forçar), `Throw` (parar), `Resolve` (resolver com `undefined`), `Default` (seguir contagem `times`).
99
+ 📖 See also: [docs/helpers.md](docs/helpers.md)
100
+
101
+ 7. **Seletores `$s` e `$$s` em Pages/Handlers**: `$s` define seletor único da página; `$$s` define mapa nomeado de seletores (`Record<string, SelectorType>`). Ambos são `abstract readonly` em `BasePage`/`BaseHandler`.
102
+ 📖 See also: [docs/crawlers.md](docs/crawlers.md)
103
+
104
+ 8. **`AttemptableInterface` — contrato base**: Tanto `BasePage` quanto `BaseHandler` implementam `AttemptableInterface`. Hooks opcionais: `success()`, `failure(exception)`, `retrying(exception, attempt)`, `finish(exception?)`, `sleep()`. Obrigatórios: `execute()`, `attempt()`.
105
+ 📖 See also: [docs/decorators.md](docs/decorators.md)
106
+
107
+ ## 💥 Critical Exceptions
108
+
109
+ | Exception | Quando é lançada | Handling |
110
+ |---|---|---|
111
+ | `BrowserException` | Falha em operação do browser em runtime (crash, perda de conexão) | Catch e retry ou fallback |
112
+ | `BrowserInstanceException` | Falha ao criar/inicializar instância do browser (extends `BrowserException`) | Verificar setup do driver, retry init |
113
+ | `RetryException` | Todas as tentativas de `retry()` esgotadas sem sucesso | Fallback final ou propagar erro |
114
+ | `TimeoutException` | Operação excede o limite de `timeout()` | Catch e tratar timeout; ajustar limite se válido |
115
+ | `InvalidArgumentException` | Parâmetros inválidos (ex: `times < 1`, timeout negativo) | Validar inputs antes de chamar API |
116
+ | `MoneyNotFoundException` | `Str.money()` não encontra valor monetário na string | Verificar formato da string antes |
117
+ | `MoneyMultipleResultException` | `Str.money()` encontra múltiplos valores; use `Str.moneys()` | Usar `moneys()` para múltiplos valores |
118
+
119
+ 📖 See also: [docs/exceptions.md](docs/exceptions.md) para referência completa com exemplos de try-catch.
120
+
121
+ ## ⚠️ Integration Pitfalls
122
+
123
+ 1. **Node 24+ obrigatório**: `engines.node >= 24.0` no package.json; versões anteriores não são suportadas.
124
+ 2. **Puppeteer/Playwright NÃO incluído**: Crawler APIs requerem driver de browser, mas o consumidor **deve instalar separadamente**. Helpers e Decorators funcionam sem driver.
125
+ 3. **DI é responsabilidade do consumidor**: Chemical-X usa Inversify mas **não auto-wira**. Consumidor deve registrar bindings e chamar `Container.loadModule()`.
126
+ 4. **Driver não é auto-selecionado**: Consumer configura qual driver usar via binding no Container ou construtor do `BrowserManager`. Puppeteer e Playwright são intercambiáveis via configuração.
127
+ 5. **`Container.loadModule()` antes de executar**: Sem essa chamada, classes registradas com `@injectablePageOrHandler` não estarão disponíveis no container.
128
+ 6. **Pages por intenção, não por URL**: Não assuma 1:1 entre URL e Page. Modele Pages pela responsabilidade/ação desejada.
129
+ 7. **Handlers não agem — validam**: Handler nunca deve interagir com a page diretamente nem chamar outras Pages. Declara Solution ou lança Exception.
130
+ 8. **Dependências de runtime**: `@odg/exception`, `inversify` e `@inversifyjs/binding-decorators` são dependências obrigatórias em runtime.
131
+
132
+ ## 📖 Detailed Documentation
133
+
134
+ | Documento | Conteúdo |
135
+ |---|---|
136
+ | [docs/helpers.md](docs/helpers.md) | Guia completo de `retry()`, `sleep()`, `timeout()`, `throwIf()` com padrões de uso |
137
+ | [docs/crawlers.md](docs/crawlers.md) | Arquitetura Crawler: BrowserManager, Pages, Handlers, Container/DI, workflow examples |
138
+ | [docs/decorators.md](docs/decorators.md) | `@attemptableFlow`, `@getterAccess`, `@injectablePageOrHandler` com lifecycle e exemplos |
139
+ | [docs/exceptions.md](docs/exceptions.md) | Referência completa de exceções com trigger, handling e exemplos |
140
+
141
+ ## 🔗 Interfaces Públicas
142
+
143
+ | Interface | Propósito |
144
+ |---|---|
145
+ | `AttemptableInterface` | Contrato base para Pages e Handlers: `execute()`, `attempt()`, hooks opcionais |
146
+ | `PageInterface` | Extends `AttemptableInterface`; contrato para `BasePage` |
147
+ | `HandlerInterface` | Extends `AttemptableInterface`; adiciona `waitForHandler()` |
148
+ | `GetterAccessInterface` | Define `__get(key, value)` para classes com `@getterAccess` |
149
+ | `CloneableInterface` | Define `clone()` para Support utilities (`Str`, `Num`, `Arr`) |
150
+ | `NativeInterface<Type>` | Define `toNative()` para conversão ao tipo primitivo |
151
+ | `RetryOptionsInterface` | Parâmetros de `retry()`: `times`, `sleep`, `callback`, `signal` |
152
+ | `TimeoutOptionsInterface` | Parâmetros de `timeout()`: `name`, `timeout`, `callback` |
153
+
154
+ ## 🔍 Entry Points
155
+
156
+ - **Main**: `import { retry, sleep, BrowserManager, BasePage, ... } from "@odg/chemical-x"`
157
+ - **Container only**: `import { Container } from "@odg/chemical-x/container"`
@@ -0,0 +1,240 @@
1
+ ## Chemical-X Crawler API - Pages, Handlers, BrowserManager
2
+
3
+ Arquitetura para automação web com abstração sobre Puppeteer/Playwright. Requer driver de browser instalado separadamente.
4
+
5
+ ```typescript
6
+ import { BrowserManager, BasePage, BaseHandler, Browser, Context, Page, Container } from "@odg/chemical-x";
7
+ ```
8
+
9
+ ---
10
+
11
+ ### BrowserManager
12
+
13
+ Orquestrador central que cria instâncias de Browser e Context via factories injetadas pelo consumidor.
14
+
15
+ **Assinatura:**
16
+
17
+ ```typescript
18
+ class BrowserManager<BrowserClassEngine, ContextClassEngine, PageClassEngine> {
19
+ constructor(
20
+ $newBrowser: CreateBrowserFactoryType<BrowserClassEngine, ContextClassEngine, PageClassEngine>,
21
+ $newContext: CreateContextFactoryType<ContextClassEngine, PageClassEngine>,
22
+ $newPage: CreatePageFactoryType<PageClassEngine>,
23
+ )
24
+
25
+ async newBrowser(
26
+ browser: () => Promise<BrowserClassEngine>,
27
+ ): Promise<BrowserChemicalXInterface<...> & BrowserClassEngine>
28
+
29
+ async newPersistentContext(
30
+ context: () => Promise<ContextClassEngine>,
31
+ ): Promise<ContextChemicalXInterface<...> & ContextClassEngine>
32
+ }
33
+ ```
34
+
35
+ **Responsabilidades:**
36
+
37
+ - Cria instâncias de `Browser` via `newBrowser()` — recebe factory que retorna engine do browser
38
+ - Cria contextos persistentes via `newPersistentContext()` — recebe factory que retorna engine do contexto
39
+ - **Não auto-seleciona driver**: consumer configura Puppeteer ou Playwright via factory injetada
40
+ - Retorna objetos que combinam interface Chemical-X com o engine nativo (via `@getterAccess` proxy)
41
+
42
+ **Lifecycle:**
43
+
44
+ 1. Consumer instancia `BrowserManager` com factories (normalmente via Container/DI)
45
+ 2. `newBrowser(() => puppeteer.launch())` → retorna `Browser` com proxy sobre Puppeteer
46
+ 3. `browser.newContext(options?)` → retorna `Context` com proxy
47
+ 4. `context.newPage()` → retorna `Page` com proxy
48
+
49
+ ---
50
+
51
+ ### Browser, Context, Page (Wrappers)
52
+
53
+ Wrappers com `@getterAccess` que delegam acesso ao engine nativo de forma transparente.
54
+
55
+ **Browser:**
56
+
57
+ ```typescript
58
+ @ODGDecorators.getterAccess()
59
+ class Browser<BrowserClassEngine, ContextClassEngine, PageClassEngine>
60
+ implements GetterAccessInterface, BrowserChemicalXInterface<...> {
61
+
62
+ $browserInstance: BrowserClassEngine;
63
+ async defaultContextOptions(): Promise<ContextOptionsLibraryInterface>;
64
+ async newContext(options?): Promise<ContextChemicalXInterface<...> & ContextClassEngine>;
65
+ contexts(): Array<ContextChemicalXInterface<...> & ContextClassEngine>;
66
+ __get(key: PropertyKey): unknown;
67
+ }
68
+ ```
69
+
70
+ **Context:**
71
+
72
+ ```typescript
73
+ @ODGDecorators.getterAccess()
74
+ class Context<ContextClassEngine, PageClassEngine>
75
+ implements GetterAccessInterface, ContextChemicalXInterface<...> {
76
+
77
+ $contextInstance: ContextClassEngine;
78
+ async defaultPageOptions(): Promise<PageOptionsLibraryInterface>;
79
+ async newPage(options?): Promise<PageChemicalXInterface<...> & PageClassEngine>;
80
+ pages(): Array<PageChemicalXInterface<...> & PageClassEngine>;
81
+ __get(key: PropertyKey): unknown;
82
+ }
83
+ ```
84
+
85
+ **Page:**
86
+
87
+ ```typescript
88
+ @ODGDecorators.getterAccess()
89
+ class Page<ContextClassEngine, PageClassEngine>
90
+ implements GetterAccessInterface, PageChemicalXInterface<...> {
91
+
92
+ $pageInstance: PageClassEngine;
93
+ context(): ContextChemicalXInterface<...> & ContextClassEngine;
94
+ __get(key: PropertyKey): unknown;
95
+ }
96
+ ```
97
+
98
+ Todos usam `@getterAccess` para delegar acessos de propriedade/método ao engine subjacente. Isso permite chamar métodos nativos do Puppeteer/Playwright diretamente no wrapper.
99
+
100
+ ---
101
+
102
+ ### BasePage (Abstract)
103
+
104
+ Define uma página por **intenção**, não por URL. Implementa `AttemptableInterface` com lifecycle hooks para retry via `@attemptableFlow`.
105
+
106
+ **Assinatura:**
107
+
108
+ ```typescript
109
+ abstract class BasePage<PageClassEngine> implements PageInterface {
110
+ currentAttempt: number = 0;
111
+ page?: PageClassEngine;
112
+
113
+ abstract readonly $s?: SelectorType; // Seletor principal
114
+ abstract readonly $$s?: Record<string | number | symbol, SelectorType>; // Mapa de seletores
115
+
116
+ setPage(page: PageClassEngine): this;
117
+
118
+ // Obrigatórios (abstract)
119
+ abstract execute(): Promise<void>; // Ação principal da página
120
+ abstract attempt(): Promise<number>; // Retorna número de tentativas
121
+
122
+ // Hooks opcionais (lifecycle do @attemptableFlow)
123
+ success?(): Promise<void>;
124
+ failure?(exception: Exception): Promise<void>;
125
+ retrying?(exception: Exception, attempt: number): Promise<RetryAction>;
126
+ finish?(exception?: Exception): Promise<void>;
127
+ sleep?(): Promise<number>;
128
+ }
129
+ ```
130
+
131
+ **Page Intent Design:**
132
+
133
+ > Pages agrupam por **INTENÇÃO**, não por URL. Uma mesma URL pode ter múltiplas Pages.
134
+
135
+ Exemplos:
136
+ - `LoginPage` (intenção: autenticar) — URL: `/login`
137
+ - `LoginVerificationPage` (intenção: verificar se login OK) — URL: `/login` ou `/dashboard`
138
+ - `HomeContentPage` (intenção: extrair conteúdo) — URL: `/`
139
+ - `HomeAdPage` (intenção: extrair anúncios) — URL: `/`
140
+
141
+ **Lifecycle:**
142
+
143
+ 1. `setPage(page)` — injeta instância da page do browser
144
+ 2. `execute()` — executa ação (navegar, preencher, clicar)
145
+ 3. Se falha → `retrying(exception, attempt)` decide retry ou não
146
+ 4. Se sucesso → `success()`
147
+ 5. Se falha final → `failure(exception)`
148
+ 6. Sempre → `finish(exception?)`
149
+
150
+ ---
151
+
152
+ ### BaseHandler (Abstract)
153
+
154
+ Valida transições e resultados de páginas. **Handlers validam, não executam ações.**
155
+
156
+ **Assinatura:**
157
+
158
+ ```typescript
159
+ abstract class BaseHandler<PageClassEngine> implements HandlerInterface {
160
+ currentAttempt: number = 0;
161
+ page?: PageClassEngine;
162
+
163
+ abstract readonly $$s: Record<string | number | symbol, SelectorType>;
164
+
165
+ setPage(page: PageClassEngine): this;
166
+
167
+ // Concrete
168
+ async execute(): Promise<void>;
169
+ async successSolution(): Promise<HandlerSolutionType>; // Retorna RetryAction.Resolve
170
+
171
+ // Obrigatórios (abstract)
172
+ abstract waitForHandler(): Promise<HandlerFunction>; // Retorna Exception ou solution function
173
+ abstract attempt(): Promise<number>;
174
+
175
+ // Hooks opcionais
176
+ success?(): Promise<void>;
177
+ failure?(exception: Exception): Promise<void>;
178
+ retrying?(exception: Exception, attempt: number): Promise<RetryAction>;
179
+ finish?(exception?: Exception): Promise<void>;
180
+ sleep?(): Promise<number>;
181
+ }
182
+ ```
183
+
184
+ **Tipos de retorno de Handler:**
185
+
186
+ ```typescript
187
+ type HandlerSolutionType = Exception | Exclude<RetryAction, RetryAction.Default | RetryAction.Throw>;
188
+ type HandlerFunction = Exception | (() => Promise<HandlerSolutionType>);
189
+ ```
190
+
191
+ **Handler Pattern:**
192
+
193
+ > "Pages executam. Handlers validam. Handlers declaram Solution ou lançam Exception."
194
+
195
+ - `waitForHandler()` retorna `Exception` → falha detectada
196
+ - `waitForHandler()` retorna `() => Promise<HandlerSolutionType>` → solução disponível
197
+ - Solution pode ser: `RetryAction.Resolve` (sucesso), `RetryAction.Retry` (tentar novamente)
198
+ - Handler **NUNCA** interage com a page diretamente
199
+ - Handler **NUNCA** chama outras Pages para resolver problemas
200
+ - Handler **NUNCA** falha silenciosamente
201
+
202
+ ---
203
+
204
+ ### Container / DI
205
+
206
+ Chemical-X usa Inversify para DI. `@injectablePageOrHandler(name)` registra metadata; consumer deve chamar `Container.loadModule()`.
207
+
208
+ **Regras:**
209
+
210
+ 1. `Container.loadModule()` **obrigatório** antes de executar qualquer Page/Handler registrado
211
+ 2. DI binding é **responsabilidade do consumidor** — Chemical-X não auto-wira
212
+ 3. `Container.getOptional(name)` retorna `undefined` se serviço não registrado (vs `get()` que lança)
213
+ 4. Puppeteer/Playwright deve ser injetado via Container ou passado ao `BrowserManager` constructor
214
+
215
+ ---
216
+
217
+ ### Workflow Example
218
+
219
+ Fluxo completo: login em site com tratamento de 2FA.
220
+
221
+ ```
222
+ 1. Create LoginPage (intent: autenticar)
223
+ → setPage(page) → execute() (navegar, preencher credenciais, clicar login)
224
+
225
+ 2. Create LoginHandler (intent: validar resultado do login)
226
+ → setPage(page) → waitForHandler()
227
+ → Detecta 2FA required → return () => new TwoFaPage() as Solution
228
+ → Detecta login error → return new InvalidCredentialsException()
229
+ → Detecta sucesso → return successSolution() (RetryAction.Resolve)
230
+
231
+ 3. Se Solution = TwoFaPage:
232
+ → Create TwoFaPage (intent: resolver 2FA)
233
+ → setPage(page) → execute() (preencher código 2FA)
234
+
235
+ 4. Create DashboardHandler (intent: verificar que chegou no dashboard)
236
+ → setPage(page) → waitForHandler()
237
+ → Verifica URL/conteúdo → return successSolution()
238
+ ```
239
+
240
+ See also: `tests/vitest/Handlers/` e `tests/vitest/Pages/` para exemplos reais de implementação.
@@ -0,0 +1,257 @@
1
+ ## Chemical-X Decorators - @attemptableFlow, @getterAccess, @injectablePageOrHandler
2
+
3
+ DSL baseada em decorators para retry com lifecycle, interceptação de acesso e DI registration.
4
+
5
+ ```typescript
6
+ import { ODGDecorators } from "@odg/chemical-x";
7
+ ```
8
+
9
+ ---
10
+
11
+ ### `@attemptableFlow()`
12
+
13
+ Decorator de classe que adiciona retry com lifecycle hooks completo. Envolve `execute()` com lógica de retentativa baseada em `AttemptableInterface`.
14
+
15
+ **Aplicação:**
16
+
17
+ ```typescript
18
+ @ODGDecorators.attemptableFlow()
19
+ class MyPage extends BasePage<PageEngine> {
20
+ // ...
21
+ }
22
+ ```
23
+
24
+ **Lifecycle Hooks:**
25
+
26
+ | Hook | Obrigatório | Quando chamado |
27
+ |---|---|---|
28
+ | `execute()` | Sim | Ação principal; retentada até `attempt()` vezes |
29
+ | `attempt()` | Sim | Retorna número máximo de tentativas |
30
+ | `sleep()` | Não | Retorna ms entre tentativas (se definido) |
31
+ | `success()` | Não | Chamado quando `execute()` sucede |
32
+ | `failure(exception)` | Não | Chamado quando todas tentativas falham; recebe a última exceção |
33
+ | `retrying(exception, attempt)` | Não | Chamado antes de cada retry; retorna `RetryAction` para decidir ação |
34
+ | `finish(exception?)` | Não | Chamado sempre ao final (sucesso ou falha); recebe exceção se houve |
35
+
36
+ **Propriedade:**
37
+
38
+ - `currentAttempt: number` — número da tentativa atual (atualizado automaticamente pelo decorator)
39
+
40
+ **Fluxo de Execução:**
41
+
42
+ ```
43
+ execute() chamado
44
+ ├─ Sucesso → success() → finish()
45
+ └─ Falha → retrying(exception, attempt)?
46
+ ├─ RetryAction.Retry → execute() novamente
47
+ ├─ RetryAction.Throw → failure(exception) → finish(exception)
48
+ ├─ RetryAction.Resolve → finish() (sem exceção, resolve undefined)
49
+ └─ RetryAction.Default → se attempts restam → sleep() → execute()
50
+ └─ se esgotou → failure(exception) → finish(exception)
51
+ ```
52
+
53
+ **Exemplo:**
54
+
55
+ ```typescript
56
+ @ODGDecorators.attemptableFlow()
57
+ class LoginPage extends BasePage<PuppeteerPage> {
58
+ readonly $s = "form#login";
59
+ readonly $$s = {
60
+ username: "input[name=username]",
61
+ password: "input[name=password]",
62
+ submit: "button[type=submit]",
63
+ };
64
+
65
+ async attempt(): Promise<number> {
66
+ return 3;
67
+ }
68
+
69
+ async execute(): Promise<void> {
70
+ await this.page!.type(this.$$s.username, "user@example.com");
71
+ await this.page!.type(this.$$s.password, "password123");
72
+ await this.page!.click(this.$$s.submit);
73
+ }
74
+
75
+ async sleep(): Promise<number> {
76
+ return 2000; // 2s entre tentativas
77
+ }
78
+
79
+ async success(): Promise<void> {
80
+ console.log("Login successful");
81
+ }
82
+
83
+ async failure(exception: Exception): Promise<void> {
84
+ console.error("Login failed after all attempts:", exception.message);
85
+ }
86
+
87
+ async retrying(exception: Exception, attempt: number): Promise<RetryAction> {
88
+ if (exception instanceof BrowserException) return RetryAction.Throw;
89
+ return RetryAction.Default;
90
+ }
91
+ }
92
+ ```
93
+
94
+ See also: `tests/vitest/Pages/` para exemplos de Pages com `@attemptableFlow`.
95
+
96
+ ---
97
+
98
+ ### `@getterAccess()`
99
+
100
+ Decorator de classe que cria um Proxy interceptando **todo** acesso a propriedade/método via `__get(key, value)`.
101
+
102
+ **Aplicação:**
103
+
104
+ ```typescript
105
+ @ODGDecorators.getterAccess()
106
+ class MyWrapper implements GetterAccessInterface {
107
+ __get(key: PropertyKey, value: unknown): unknown {
108
+ // Intercepta todo acesso
109
+ return value;
110
+ }
111
+ }
112
+ ```
113
+
114
+ **Comportamento do Proxy:**
115
+
116
+ - **Todo** acesso a propriedade (leitura) passa por `__get(key, value)` onde:
117
+ - `key` = nome da propriedade acessada
118
+ - `value` = valor original da propriedade (se existir)
119
+ - Permite: delegação transparente ao engine, validação, lazy-load, tracking, logging
120
+
121
+ **Uso interno no Chemical-X:**
122
+
123
+ `Browser`, `Context` e `Page` usam `@getterAccess` para delegar ao engine nativo:
124
+
125
+ ```typescript
126
+ @ODGDecorators.getterAccess()
127
+ class Browser<BrowserClassEngine, ...> implements GetterAccessInterface {
128
+ $browserInstance: BrowserClassEngine;
129
+
130
+ __get(key: PropertyKey): unknown {
131
+ // Delega ao engine nativo (Puppeteer/Playwright)
132
+ return (this.$browserInstance as Record<PropertyKey, unknown>)[key];
133
+ }
134
+ }
135
+ ```
136
+
137
+ Isso permite:
138
+ ```typescript
139
+ const browser = await manager.newBrowser(() => puppeteer.launch());
140
+ // Acessa métodos nativos do Puppeteer diretamente no wrapper:
141
+ await browser.close(); // Delegado via __get → puppeteerBrowser.close()
142
+ ```
143
+
144
+ See also: `tests/vitest/puppeteer/` e `tests/vitest/playwright/` para exemplos de uso com drivers reais.
145
+
146
+ ---
147
+
148
+ ### `@injectablePageOrHandler(name)`
149
+
150
+ Registra classe no Container (Inversify) com `@injectable()` e `@injectFromHierarchy()`.
151
+
152
+ **Aplicação:**
153
+
154
+ ```typescript
155
+ @ODGDecorators.injectablePageOrHandler("LoginPage")
156
+ @ODGDecorators.attemptableFlow()
157
+ class LoginPage extends BasePage<PageEngine> {
158
+ // ...
159
+ }
160
+ ```
161
+
162
+ **Regras:**
163
+
164
+ 1. Define metadata para DI — **não registra automaticamente no Container**
165
+ 2. Consumer **deve** chamar `Container.loadModule()` para efetivar os bindings
166
+ 3. Combinado com `@attemptableFlow` em Pages e Handlers
167
+ 4. O `name` é usado como identificador do binding no Container
168
+
169
+ ---
170
+
171
+ ### `@registerListener(eventName, containerName, options)`
172
+
173
+ Registra listener de eventos em Container EventEmitter via metadata.
174
+
175
+ **Aplicação:**
176
+
177
+ ```typescript
178
+ @ODGDecorators.registerListener("page:loaded", "PageEventEmitter", { once: true })
179
+ class PageLoadedListener {
180
+ // ...
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ### Comparison - When to Use
187
+
188
+ | Cenário | Ferramenta | Motivo |
189
+ |---|---|---|
190
+ | Retry simples de callback | `retry()` | Função isolada, sem lifecycle, sem estado |
191
+ | Retry de classe inteira com hooks | `@attemptableFlow` | Lifecycle completo: attempt, success, failure, retrying, finish, sleep |
192
+ | Interceptar propriedades | `@getterAccess` | Proxy transparente para delegação/validação |
193
+ | Registrar no Container | `@injectablePageOrHandler` | DI via Inversify com hierarchy |
194
+ | Callback com timeout | `timeout()` | Sem retry, apenas limite de tempo |
195
+ | Classe retriable com timeout | `@attemptableFlow` + `timeout()` no `execute()` | Combine ambos para retry + timeout |
196
+
197
+ **Regra geral:**
198
+ - Use `retry()` quando quer retentar **uma função**
199
+ - Use `@attemptableFlow` quando quer retentar **um comportamento de classe** com estado e lifecycle
200
+
201
+ ---
202
+
203
+ ### Common Patterns
204
+
205
+ **@attemptableFlow com handler e solução:**
206
+
207
+ ```typescript
208
+ @ODGDecorators.injectablePageOrHandler("LoginHandler")
209
+ @ODGDecorators.attemptableFlow()
210
+ class LoginHandler extends BaseHandler<PuppeteerPage> {
211
+ readonly $$s = {
212
+ errorMsg: ".error-message",
213
+ dashboard: "#dashboard",
214
+ twoFa: "#two-fa-form",
215
+ };
216
+
217
+ async attempt(): Promise<number> {
218
+ return 5;
219
+ }
220
+
221
+ async waitForHandler(): Promise<HandlerFunction> {
222
+ // Espera por um dos seletores aparecer na página
223
+ const found = await this.page!.waitForSelector(
224
+ [this.$$s.errorMsg, this.$$s.dashboard, this.$$s.twoFa].join(", "),
225
+ );
226
+
227
+ if (found?.matches(this.$$s.errorMsg)) {
228
+ return new BrowserException("Login failed");
229
+ }
230
+
231
+ if (found?.matches(this.$$s.twoFa)) {
232
+ return async () => RetryAction.Retry; // Indica que precisa retry (2FA page)
233
+ }
234
+
235
+ return async () => this.successSolution(); // RetryAction.Resolve
236
+ }
237
+ }
238
+ ```
239
+
240
+ **@getterAccess para wrapper customizado:**
241
+
242
+ ```typescript
243
+ @ODGDecorators.getterAccess()
244
+ class CachedPage implements GetterAccessInterface {
245
+ private cache = new Map<PropertyKey, unknown>();
246
+ private wrapped: SomeEngine;
247
+
248
+ __get(key: PropertyKey, value: unknown): unknown {
249
+ if (this.cache.has(key)) return this.cache.get(key);
250
+ const result = (this.wrapped as Record<PropertyKey, unknown>)[key];
251
+ this.cache.set(key, result);
252
+ return result;
253
+ }
254
+ }
255
+ ```
256
+
257
+ See also: `tests/vitest/Handlers/` e `tests/vitest/Listeners/` para exemplos reais.
@@ -0,0 +1,212 @@
1
+ ## Chemical-X Exceptions - Reference Guide
2
+
3
+ Referência completa de todas as exceções públicas. Todas estendem `Exception` de `@odg/exception`.
4
+
5
+ ```typescript
6
+ import {
7
+ BrowserException,
8
+ BrowserInstanceException,
9
+ RetryException,
10
+ TimeoutException,
11
+ InvalidArgumentException,
12
+ MoneyNotFoundException,
13
+ MoneyMultipleResultException,
14
+ } from "@odg/chemical-x";
15
+ ```
16
+
17
+ ---
18
+
19
+ ### Exception Reference
20
+
21
+ #### `BrowserException`
22
+
23
+ | | |
24
+ |---|---|
25
+ | **Trigger** | Falha em operação do browser em runtime (crash, perda de conexão, operação inválida) |
26
+ | **Quando** | Interações com Page/Browser via Crawler API (click, type, goto, etc.) |
27
+ | **Extends** | `Exception` (`@odg/exception`) |
28
+ | **Handling** | Catch e retry (via `@attemptableFlow`) ou fallback |
29
+
30
+ ```typescript
31
+ try {
32
+ await page.execute();
33
+ } catch (exception) {
34
+ if (exception instanceof BrowserException) {
35
+ // Browser crashed ou conexão perdida — retry ou fallback
36
+ }
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ #### `BrowserInstanceException`
43
+
44
+ | | |
45
+ |---|---|
46
+ | **Trigger** | Falha ao criar/inicializar instância do browser |
47
+ | **Quando** | `BrowserManager.newBrowser()` ou `BrowserManager.newPersistentContext()` |
48
+ | **Extends** | `BrowserException` |
49
+ | **Handling** | Verificar setup do driver (Puppeteer/Playwright instalado?), retry init |
50
+
51
+ ```typescript
52
+ try {
53
+ const browser = await manager.newBrowser(() => puppeteer.launch());
54
+ } catch (exception) {
55
+ if (exception instanceof BrowserInstanceException) {
56
+ // Driver não encontrado ou falha ao iniciar — verificar instalação
57
+ }
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ #### `RetryException`
64
+
65
+ | | |
66
+ |---|---|
67
+ | **Trigger** | Todas as tentativas de `retry()` esgotadas sem sucesso |
68
+ | **Quando** | `retry()` completa todos os `times` sem callback suceder |
69
+ | **Extends** | `Exception` (`@odg/exception`) |
70
+ | **Handling** | Fallback final ou propagar erro ao chamador |
71
+
72
+ ```typescript
73
+ try {
74
+ await retry({ times: 3, callback: async () => { /* ... */ } });
75
+ } catch (exception) {
76
+ if (exception instanceof RetryException) {
77
+ // Todas tentativas falharam — aplicar fallback ou logar erro final
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ #### `TimeoutException`
85
+
86
+ | | |
87
+ |---|---|
88
+ | **Trigger** | Operação excede o limite de tempo definido em `timeout()` |
89
+ | **Quando** | `timeout({ timeout: ms, callback })` quando callback não completa a tempo |
90
+ | **Extends** | `Exception` (`@odg/exception`) |
91
+ | **Handling** | Catch e tratar timeout; ajustar limite se operação legitimamente lenta |
92
+
93
+ ```typescript
94
+ try {
95
+ await timeout({
96
+ name: "loadPage",
97
+ timeout: 5000,
98
+ callback: async () => await page.goto(url),
99
+ });
100
+ } catch (exception) {
101
+ if (exception instanceof TimeoutException) {
102
+ // Operação "loadPage" excedeu 5000ms — retry ou aumentar limite
103
+ }
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ #### `InvalidArgumentException`
110
+
111
+ | | |
112
+ |---|---|
113
+ | **Trigger** | Parâmetros inválidos passados para API |
114
+ | **Quando** | `retry({ times: -1 })`, timeout negativo, argumento obrigatório ausente |
115
+ | **Extends** | `Exception` (`@odg/exception`) |
116
+ | **Handling** | Validar inputs antes de chamar API; este é um erro de programação |
117
+
118
+ ```typescript
119
+ // Este erro indica bug no código do consumidor:
120
+ try {
121
+ await retry({ times: 0, callback: async () => {} });
122
+ } catch (exception) {
123
+ if (exception instanceof InvalidArgumentException) {
124
+ // Corrigir: times deve ser >= 1
125
+ }
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ #### `MoneyNotFoundException`
132
+
133
+ | | |
134
+ |---|---|
135
+ | **Trigger** | `Str.money()` não encontra valor monetário na string |
136
+ | **Quando** | `new Str("texto sem valor").money()` |
137
+ | **Extends** | `Exception` (`@odg/exception`) |
138
+ | **Handling** | Verificar formato da string antes; usar try-catch ou validar conteúdo |
139
+
140
+ ```typescript
141
+ try {
142
+ const value = new Str(rawText).money();
143
+ } catch (exception) {
144
+ if (exception instanceof MoneyNotFoundException) {
145
+ // String não contém valor monetário reconhecível
146
+ }
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ #### `MoneyMultipleResultException`
153
+
154
+ | | |
155
+ |---|---|
156
+ | **Trigger** | `Str.money()` encontra múltiplos valores monetários na string |
157
+ | **Quando** | `new Str("R$ 10,00 e R$ 20,00").money()` — use `moneys()` |
158
+ | **Extends** | `Exception` (`@odg/exception`) |
159
+ | **Handling** | Usar `Str.moneys()` para extrair todos os valores; `money()` espera exatamente um |
160
+
161
+ ```typescript
162
+ try {
163
+ const value = new Str("R$ 10,00 e R$ 20,00").money();
164
+ } catch (exception) {
165
+ if (exception instanceof MoneyMultipleResultException) {
166
+ // Múltiplos valores encontrados — usar moneys() em vez de money()
167
+ const values = new Str("R$ 10,00 e R$ 20,00").moneys();
168
+ }
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Handler Exception/Solution Contract
175
+
176
+ `BaseHandler.waitForHandler()` retorna `HandlerFunction`:
177
+
178
+ ```typescript
179
+ type HandlerSolutionType = Exception | Exclude<RetryAction, RetryAction.Default | RetryAction.Throw>;
180
+ type HandlerFunction = Exception | (() => Promise<HandlerSolutionType>);
181
+ ```
182
+
183
+ **Contrato:**
184
+
185
+ 1. Handler **deve** retornar `Exception` (falha) ou `() => Promise<HandlerSolutionType>` (solução)
186
+ 2. Handler **nunca** falha silenciosamente (retornar `void`/`undefined` não permitido)
187
+ 3. Solution function retorna:
188
+ - `RetryAction.Resolve` — sucesso (via `successSolution()`)
189
+ - `RetryAction.Retry` — retentar
190
+ - `Exception` — falha específica detectada na validação
191
+
192
+ **Exemplo — Handler com contrato completo:**
193
+
194
+ ```typescript
195
+ async waitForHandler(): Promise<HandlerFunction> {
196
+ // Espera seletores e decide
197
+ const errorVisible = await this.page!.$(this.$$s.error);
198
+ if (errorVisible) {
199
+ return new BrowserException("Login failed: error message visible");
200
+ }
201
+
202
+ const dashboardVisible = await this.page!.$(this.$$s.dashboard);
203
+ if (dashboardVisible) {
204
+ return async () => this.successSolution(); // RetryAction.Resolve
205
+ }
206
+
207
+ // Estado intermediário — retentar
208
+ return async () => RetryAction.Retry;
209
+ }
210
+ ```
211
+
212
+ See also: `tests/vitest/Exceptions/` e `tests/vitest/Handlers/` para exemplos de teste.
@@ -0,0 +1,223 @@
1
+ ## Chemical-X Helpers - Detailed Guide
2
+
3
+ Funções utilitárias para controle de fluxo assíncrono com tratamento de erros tipado. Não requerem browser driver.
4
+
5
+ ```typescript
6
+ import { retry, sleep, timeout, throwIf, RetryAction } from "@odg/chemical-x";
7
+ ```
8
+
9
+ ---
10
+
11
+ ### `retry(options)`
12
+
13
+ Retenta um callback N vezes com controle fino sobre sleep, abort e decisão por tentativa.
14
+
15
+ **Assinatura:**
16
+
17
+ ```typescript
18
+ // Quando `when` retorna Default | Retry | Throw (ou não fornecido): retorna ReturnType
19
+ async function retry<ReturnType>(options: {
20
+ times: number;
21
+ sleep?: number;
22
+ signal?: AbortSignal;
23
+ callback(attempt: number, signal?: AbortSignal): Promise<ReturnType> | ReturnType;
24
+ when?(exception: Exception, times: number): Promise<RetryAction> | RetryAction;
25
+ }): Promise<ReturnType>;
26
+
27
+ // Quando `when` pode retornar Resolve: retorno inclui undefined
28
+ async function retry<ReturnType>(options: {
29
+ times: number;
30
+ sleep?: number;
31
+ signal?: AbortSignal;
32
+ callback(attempt: number, signal?: AbortSignal): Promise<ReturnType> | ReturnType;
33
+ when?(exception: Exception, times: number): Promise<RetryAction> | RetryAction;
34
+ }): Promise<ReturnType | undefined>;
35
+ ```
36
+
37
+ **Parâmetros:**
38
+
39
+ | Parâmetro | Tipo | Obrigatório | Descrição |
40
+ |---|---|---|---|
41
+ | `times` | `number` | Sim | Número máximo de tentativas. `InvalidArgumentException` se < 1 |
42
+ | `sleep` | `number` | Não | Milissegundos entre tentativas |
43
+ | `signal` | `AbortSignal` | Não | Sinal de abort para cancelar retries |
44
+ | `callback` | `(attempt, signal?) => T` | Sim | Função a ser retentada. Recebe número da tentativa atual |
45
+ | `when` | `(exception, times) => RetryAction` | Não | Decide ação por tentativa: `Retry`, `Throw`, `Resolve`, `Default` |
46
+
47
+ **Comportamento:**
48
+
49
+ 1. Executa `callback(attempt, signal)` até `times` tentativas
50
+ 2. Se callback sucede → retorna resultado
51
+ 3. Se callback falha e `when` está definido → chama `when(exception, remainingTimes)`:
52
+ - `RetryAction.Retry` → retenta imediatamente (ignora `times`)
53
+ - `RetryAction.Throw` → lança exceção imediatamente
54
+ - `RetryAction.Resolve` → resolve com `undefined`
55
+ - `RetryAction.Default` → segue contagem normal de `times`
56
+ 4. Se todas tentativas esgotam → lança `RetryException`
57
+ 5. Entre tentativas, aguarda `sleep` ms (se definido)
58
+
59
+ **Exemplo:**
60
+
61
+ ```typescript
62
+ const data = await retry({
63
+ times: 3,
64
+ sleep: 1000,
65
+ callback: async (attempt) => {
66
+ return await fetchData(attempt);
67
+ },
68
+ when: (exception, times) => {
69
+ if (exception instanceof FatalError) return RetryAction.Throw;
70
+ return RetryAction.Default;
71
+ },
72
+ });
73
+ ```
74
+
75
+ See also: `tests/vitest/helpers/retry.test.ts` para exemplos completos de padrões com retry.
76
+
77
+ ---
78
+
79
+ ### `sleep(milliseconds, options?)`
80
+
81
+ Pausa assíncrona com suporte a cancelamento via `AbortSignal`.
82
+
83
+ **Assinatura:**
84
+
85
+ ```typescript
86
+ async function sleep(milliseconds: number, options?: { signal?: AbortSignal }): Promise<void>
87
+ ```
88
+
89
+ **Parâmetros:**
90
+
91
+ | Parâmetro | Tipo | Obrigatório | Descrição |
92
+ |---|---|---|---|
93
+ | `milliseconds` | `number` | Sim | Duração da pausa em milissegundos |
94
+ | `options.signal` | `AbortSignal` | Não | Sinal para cancelar o sleep antecipadamente |
95
+
96
+ **Exemplo:**
97
+
98
+ ```typescript
99
+ await sleep(2000); // Pausa 2 segundos
100
+
101
+ // Com abort
102
+ const controller = new AbortController();
103
+ await sleep(5000, { signal: controller.signal });
104
+ ```
105
+
106
+ ---
107
+
108
+ ### `timeout(options)`
109
+
110
+ Envolve uma operação assíncrona com limite de tempo. Lança `TimeoutException` se exceder.
111
+
112
+ **Assinatura:**
113
+
114
+ ```typescript
115
+ async function timeout<ReturnType>(options: {
116
+ name?: string;
117
+ timeout?: number;
118
+ callback(): Promise<ReturnType> | ReturnType;
119
+ }): Promise<ReturnType>
120
+ ```
121
+
122
+ **Parâmetros:**
123
+
124
+ | Parâmetro | Tipo | Obrigatório | Descrição |
125
+ |---|---|---|---|
126
+ | `name` | `string` | Não | Nome da operação (incluído na mensagem de `TimeoutException`) |
127
+ | `timeout` | `number` | Não | Limite em milissegundos |
128
+ | `callback` | `() => T` | Sim | Operação a ser executada com limite de tempo |
129
+
130
+ **Comportamento:**
131
+
132
+ - Se callback completa antes do limite → retorna resultado
133
+ - Se excede limite → lança `TimeoutException` com nome da operação (se fornecido)
134
+
135
+ **Exemplo:**
136
+
137
+ ```typescript
138
+ const result = await timeout({
139
+ name: "fetchData",
140
+ timeout: 5000,
141
+ callback: async () => {
142
+ return await longRunningOperation();
143
+ },
144
+ });
145
+ ```
146
+
147
+ See also: `tests/vitest/helpers/timeout.test.ts` para exemplos com timeout.
148
+
149
+ ---
150
+
151
+ ### `throwIf(condition, exception)`
152
+
153
+ Lança exceção condicionalmente com tipagem estrita — `never` quando condition é `true`.
154
+
155
+ **Assinatura:**
156
+
157
+ ```typescript
158
+ function throwIf(condition: true, exception: () => Exception): never;
159
+ function throwIf(condition: false, exception: () => Exception): void;
160
+ function throwIf(condition: boolean, exception: () => Exception): never | void;
161
+ ```
162
+
163
+ **Parâmetros:**
164
+
165
+ | Parâmetro | Tipo | Obrigatório | Descrição |
166
+ |---|---|---|---|
167
+ | `condition` | `boolean` | Sim | Se `true`, lança a exceção |
168
+ | `exception` | `() => Exception` | Sim | Factory que cria a exceção (lazy — só chamada se `condition: true`) |
169
+
170
+ **Exemplo:**
171
+
172
+ ```typescript
173
+ throwIf(!user, () => new InvalidArgumentException("User is required"));
174
+ // Após esta linha, TypeScript sabe que `user` existe (type narrowing via `never`)
175
+
176
+ throwIf(amount < 0, () => new InvalidArgumentException("Amount must be positive"));
177
+ ```
178
+
179
+ See also: `tests/vitest/helpers/throw-if.test.ts` para padrões de validação.
180
+
181
+ ---
182
+
183
+ ### Common Patterns
184
+
185
+ **Retry com exponential backoff:**
186
+
187
+ ```typescript
188
+ let delay = 500;
189
+ const result = await retry({
190
+ times: 5,
191
+ sleep: delay,
192
+ callback: async (attempt) => {
193
+ delay = 500 * Math.pow(2, attempt - 1); // 500, 1000, 2000, 4000, 8000
194
+ return await unstableApi();
195
+ },
196
+ });
197
+ ```
198
+
199
+ **Retry com timeout por tentativa:**
200
+
201
+ ```typescript
202
+ const result = await retry({
203
+ times: 3,
204
+ sleep: 1000,
205
+ callback: async (attempt) => {
206
+ return await timeout({
207
+ name: `attempt-${attempt}`,
208
+ timeout: 5000,
209
+ callback: () => fetchData(),
210
+ });
211
+ },
212
+ });
213
+ ```
214
+
215
+ **Validação com throwIf encadeado:**
216
+
217
+ ```typescript
218
+ throwIf(!config.url, () => new InvalidArgumentException("URL is required"));
219
+ throwIf(config.timeout < 0, () => new InvalidArgumentException("Timeout must be positive"));
220
+ // config agora está validado com tipagem correta
221
+ ```
222
+
223
+ See also: `tests/vitest/helpers/` para todos os testes de helpers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odg/chemical-x",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Chemical-X Project It's the basis of everything",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,9 @@
30
30
  },
31
31
  "files": [
32
32
  "./dist/",
33
- "./README.md"
33
+ "./README.md",
34
+ "agents.md",
35
+ "docs/**"
34
36
  ],
35
37
  "author": "Dragons Gamers <https://www.linkedin.com/in/victor-alves-odgodinho>",
36
38
  "license": "MIT",