@odg/chemical-x 2.1.3 → 2.3.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 +158 -0
- package/dist/Support/Decorators/OdgDecorators.d.ts +3 -2
- package/dist/Support/Decorators/OdgDecorators.js +6 -6
- package/dist/Support/Decorators/OdgDecorators.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/crawlers.md +240 -0
- package/docs/decorators.md +258 -0
- package/docs/exceptions.md +212 -0
- package/docs/helpers.md +223 -0
- package/package.json +4 -2
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. `@ODGDecorators.injectable(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,258 @@
|
|
|
1
|
+
## Chemical-X Decorators - @ODGDecorators.attemptableFlow, @ODGDecorators.getterAccess, @ODGDecorators.injectable
|
|
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
|
+
### `@ODGDecorators.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
|
+
### `@ODGDecorators.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
|
+
### `@ODGDecorators.injectable(name, scope?)`
|
|
149
|
+
|
|
150
|
+
Registra classe no Container (Inversify) com `@injectable()` e `@injectFromHierarchy()`.
|
|
151
|
+
|
|
152
|
+
**Aplicação:**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
@ODGDecorators.injectable("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
|
+
5. Deve sempre ficar a cima de todos os outros decorators (ex: `@attemptableFlow`) para garantir que a classe seja registrada apos de ser processada por outros decorators
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### `@registerListener(eventName, containerName, options)`
|
|
173
|
+
|
|
174
|
+
Registra listener de eventos em Container EventEmitter via metadata.
|
|
175
|
+
|
|
176
|
+
**Aplicação:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
@ODGDecorators.registerListener("page:loaded", "PageEventEmitter", { once: true })
|
|
180
|
+
class PageLoadedListener {
|
|
181
|
+
// ...
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### Comparison - When to Use
|
|
188
|
+
|
|
189
|
+
| Cenário | Ferramenta | Motivo |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| Retry simples de callback | `retry()` | Função isolada, sem lifecycle, sem estado |
|
|
192
|
+
| Retry de classe inteira com hooks | `@ODGDecorators.attemptableFlow` | Lifecycle completo: attempt, success, failure, retrying, finish, sleep |
|
|
193
|
+
| Interceptar propriedades | `@ODGDecorators.getterAccess` | Proxy transparente para delegação/validação |
|
|
194
|
+
| Registrar no Container | `@ODGDecorators.injectable` | DI via Inversify com hierarchy |
|
|
195
|
+
| Callback com timeout | `timeout()` | Sem retry, apenas limite de tempo |
|
|
196
|
+
| Classe retriable com timeout | `@ODGDecorators.attemptableFlow` + `timeout()` no `execute()` | Combine ambos para retry + timeout |
|
|
197
|
+
|
|
198
|
+
**Regra geral:**
|
|
199
|
+
- Use `retry()` quando quer retentar **uma função**
|
|
200
|
+
- Use `@ODGDecorators.attemptableFlow` quando quer retentar **um comportamento de classe** com estado e lifecycle
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### Common Patterns
|
|
205
|
+
|
|
206
|
+
**@ODGDecorators.attemptableFlow com handler e solução:**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
@ODGDecorators.injectable("LoginHandler")
|
|
210
|
+
@ODGDecorators.attemptableFlow()
|
|
211
|
+
class LoginHandler extends BaseHandler<PuppeteerPage> {
|
|
212
|
+
readonly $$s = {
|
|
213
|
+
errorMsg: ".error-message",
|
|
214
|
+
dashboard: "#dashboard",
|
|
215
|
+
twoFa: "#two-fa-form",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
async attempt(): Promise<number> {
|
|
219
|
+
return 5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async waitForHandler(): Promise<HandlerFunction> {
|
|
223
|
+
// Espera por um dos seletores aparecer na página
|
|
224
|
+
const found = await this.page!.waitForSelector(
|
|
225
|
+
[this.$$s.errorMsg, this.$$s.dashboard, this.$$s.twoFa].join(", "),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (found?.matches(this.$$s.errorMsg)) {
|
|
229
|
+
return new BrowserException("Login failed");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (found?.matches(this.$$s.twoFa)) {
|
|
233
|
+
return async () => RetryAction.Retry; // Indica que precisa retry (2FA page)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return async () => this.successSolution(); // RetryAction.Resolve
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**@ODGDecorators.getterAccess para wrapper customizado:**
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
@ODGDecorators.getterAccess()
|
|
245
|
+
class CachedPage implements GetterAccessInterface {
|
|
246
|
+
private cache = new Map<PropertyKey, unknown>();
|
|
247
|
+
private wrapped: SomeEngine;
|
|
248
|
+
|
|
249
|
+
__get(key: PropertyKey, value: unknown): unknown {
|
|
250
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
251
|
+
const result = (this.wrapped as Record<PropertyKey, unknown>)[key];
|
|
252
|
+
this.cache.set(key, result);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
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.
|