@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 +2 -0
- package/agents.md +157 -0
- package/docs/crawlers.md +240 -0
- package/docs/decorators.md +257 -0
- package/docs/exceptions.md +212 -0
- package/docs/helpers.md +223 -0
- package/package.json +4 -2
package/README.md
CHANGED
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"`
|
package/docs/crawlers.md
ADDED
|
@@ -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.
|
package/docs/helpers.md
ADDED
|
@@ -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.
|
|
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",
|