@wenathlan/saddle 1.8.2 → 1.8.5

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.
Files changed (257) hide show
  1. package/README.md +9 -9
  2. package/browser/playwright.js +22 -0
  3. package/docs/.gitkeep +0 -0
  4. package/docs/gapmatrix.md +3 -3
  5. package/docs/libraryapi.md +1 -0
  6. package/docs/logs/.gitkeep +0 -0
  7. package/docs/packageaudit185.md +23 -0
  8. package/docs/plans/00.index.md +50 -0
  9. package/docs/plans/01.architecture.md +86 -0
  10. package/docs/plans/02.research.computer.use.md +58 -0
  11. package/docs/plans/03.research.captcha.bypass.md +68 -0
  12. package/docs/plans/04.research.sandbox.ai.md +52 -0
  13. package/docs/plans/05.capture.platform.md +57 -0
  14. package/docs/plans/06.dependencies.md +97 -0
  15. package/docs/plans/07.captcha.test.page.md +41 -0
  16. package/docs/plans/08.production.infra.md +70 -0
  17. package/docs/plans/09.database.schema.md +121 -0
  18. package/docs/plans/10.cloudinary.storage.md +57 -0
  19. package/docs/plans/11.movement.logs.json.md +72 -0
  20. package/docs/plans/12.research.atlas.agent.browser.md +79 -0
  21. package/docs/plans/13.research.anti.detection.md +898 -0
  22. package/docs/plans/14.research.proxy.md +1495 -0
  23. package/docs/plans/15.research.retry.rate.limit.md +1958 -0
  24. package/docs/plans/16.research.crawling.md +1417 -0
  25. package/docs/plans/17.research.caching.md +1610 -0
  26. package/docs/plans/18.research.content.extraction.md +1952 -0
  27. package/docs/plans/19.research.errors.events.md +1523 -0
  28. package/docs/plans/20.research.zod.validation.md +1350 -0
  29. package/docs/plans/21.research.batch.concurrency.md +1888 -0
  30. package/docs/plans/22.research.universal.runtime.md +944 -0
  31. package/docs/plans/23.research.ai.integration.md +1465 -0
  32. package/docs/plans/24.research.memory.persistence.md +1979 -0
  33. package/docs/plans/25.research.server.api.md +342 -0
  34. package/docs/plans/26.research.compilation.md +249 -0
  35. package/docs/plans/27.research.html.parsing.md +251 -0
  36. package/docs/plans/28.action.plan.md +50 -0
  37. package/docs/plans/29.api.reference.md +174 -0
  38. package/docs/plans/30.architecture.plan.md +94 -0
  39. package/docs/plans/31.auditoria.dados.md +163 -0
  40. package/docs/plans/32.bots.automacao.computacional.md +214 -0
  41. package/docs/plans/33.bots.codigo.revisao.md +220 -0
  42. package/docs/plans/34.bots.seguranca.cicd.md +366 -0
  43. package/docs/plans/35.comparativo.concorrencia.md +464 -0
  44. package/docs/plans/36.computational.memory.md +340 -0
  45. package/docs/plans/37.deploystrategy.md +394 -0
  46. package/docs/plans/38.flow.md +155 -0
  47. package/docs/plans/39.multi.platform.bot.md +252 -0
  48. package/docs/plans/40.npm.publish.md +250 -0
  49. package/docs/plans/41.o.que.falta.md +407 -0
  50. package/docs/plans/42.pesquisa.concorrencia.md +721 -0
  51. package/docs/plans/43.plan.universal.architecture.md +496 -0
  52. package/docs/plans/44.reference.md +100 -0
  53. package/docs/plans/45.robotarchitecture.md +237 -0
  54. package/docs/plans/46.scdnintegration.md +284 -0
  55. package/docs/plans/47.multiforge.readme.md +129 -0
  56. package/docs/plans/48.theory.v4.repo.os.md +152 -0
  57. package/docs/plans/49.third.party.infra.md +12 -0
  58. package/docs/plans/50.file.as.compute.md +39 -0
  59. package/docs/plans/51.architecture.virtual.processor.md +80 -0
  60. package/docs/plans/52.manifesto.v8.md +11 -0
  61. package/docs/plans/58.cdn.list.md +23 -0
  62. package/docs/plans/59.sql.frameworks.md +33 -0
  63. package/docs/plans/60.sql.thirdparty.md +26 -0
  64. package/docs/plans/61.objective.multiforge.md +63 -0
  65. package/docs/plans/62.huggingface.upload.md +26 -0
  66. package/docs/plans/63.kaggle.upload.md +24 -0
  67. package/docs/plans/64.npm.storage.md +30 -0
  68. package/docs/plans/65.rclone.terabox.md +32 -0
  69. package/docs/plans/66.buckets.and.models.todo.md +14 -0
  70. package/docs/plans/67.database.todo.md +13 -0
  71. package/docs/plans/68.deploy.packages.todo.md +12 -0
  72. package/docs/plans/69.report.human.operator.md +133 -0
  73. package/docs/plans/70.report.brain2qwerty.ems.md +135 -0
  74. package/docs/plans/71.report.hd.infinito.vram.md +155 -0
  75. package/docs/plans/72.plan.hd.infinito.node.md +146 -0
  76. package/docs/plans/73.plan.scifi.repos.md +125 -0
  77. package/docs/plans/74.000.manifesto.v8.flat.2..md +11 -0
  78. package/docs/plans/README.md +489 -0
  79. package/docs/plans/aggregate_platforms.mjs +146 -0
  80. package/docs/plans/examplesession.json +36 -0
  81. package/docs/plans/missing-facts.md +192 -0
  82. package/docs/plans/models.md +64 -0
  83. package/docs/plans/organize.cjs +270 -0
  84. package/docs/plans/platforms.md +2887 -0
  85. package/docs/plans/sites.md +31322 -0
  86. package/docs/platformpipelineaudit.md +18 -0
  87. package/docs/platformpipelines.md +13 -0
  88. package/docs/registryresearch.md +4 -0
  89. package/docs/release.md +4 -4
  90. package/docs/release182notes.md +2 -0
  91. package/docs/release184notes.md +7 -0
  92. package/docs/release185notes.md +7 -0
  93. package/docs/sources/farm.py +117 -0
  94. package/docs/sources/html/saddle1.html +132 -0
  95. package/docs/sources/html/saddle2.html +157 -0
  96. package/docs/sources/html/saddle3.html +119 -0
  97. package/docs/sources/html/saddle4.html +144 -0
  98. package/docs/sources/html/saddle5.html +72 -0
  99. package/docs/sources/html/saddle6.html +171 -0
  100. package/docs/sources/html/saddle7.html +236 -0
  101. package/docs/sources/saddle.ts +74 -0
  102. package/docs/sources/schema.prisma +88 -0
  103. package/docs/sources/script.sh +64 -0
  104. package/docs/sources/workflows.yml +458 -0
  105. package/docs/talks1/_body.txt +14 -0
  106. package/docs/talks1/_index.md +15 -0
  107. package/docs/talks1/_screenshot.png +0 -0
  108. package/docs/talks1/assistant-01.md +5 -0
  109. package/docs/talks1/assistant-02.md +5 -0
  110. package/docs/talks1/assistant-03.md +531 -0
  111. package/docs/talks1/assistant-04.md +26 -0
  112. package/docs/talks1/assistant-05.md +774 -0
  113. package/docs/talks1/assistant-06.md +1718 -0
  114. package/docs/talks1/scrape-share.cjs +185 -0
  115. package/docs/talks1/scrape-share.ts +183 -0
  116. package/docs/talks1/user-01.md +3 -0
  117. package/docs/talks1/user-02.md +3 -0
  118. package/docs/talks1/user-03.md +88 -0
  119. package/docs/talks1/user-04.md +3 -0
  120. package/docs/talks1/user-05.md +3 -0
  121. package/docs/talks1/user-06.md +88 -0
  122. package/docs/talks1/user-07.md +88 -0
  123. package/docs/talks2/_body.txt +14 -0
  124. package/docs/talks2/_index.md +16 -0
  125. package/docs/talks2/_screenshot.png +0 -0
  126. package/docs/talks2/assistant-01.md +5 -0
  127. package/docs/talks2/assistant-02.md +5 -0
  128. package/docs/talks2/assistant-03.md +424 -0
  129. package/docs/talks2/assistant-04.md +598 -0
  130. package/docs/talks2/assistant-05.md +1280 -0
  131. package/docs/talks2/assistant-06.md +1227 -0
  132. package/docs/talks2/assistant-07.md +1252 -0
  133. package/docs/talks2/user-01.md +3 -0
  134. package/docs/talks2/user-02.md +3 -0
  135. package/docs/talks2/user-03.md +88 -0
  136. package/docs/talks2/user-04.md +88 -0
  137. package/docs/talks2/user-05.md +88 -0
  138. package/docs/talks2/user-06.md +88 -0
  139. package/docs/talks2/user-07.md +3 -0
  140. package/docs/talks3/_body.txt +467 -0
  141. package/docs/talks3/_index.md +10 -0
  142. package/docs/talks3/_screenshot.png +0 -0
  143. package/docs/talks3/assistant-01.md +417 -0
  144. package/docs/talks3/assistant-02.md +417 -0
  145. package/docs/talks3/assistant-03.md +29 -0
  146. package/docs/talks3/assistant-04.md +727 -0
  147. package/docs/talks3/user-01.md +88 -0
  148. package/docs/talks3/user-02.md +88 -0
  149. package/docs/talks3/user-03.md +3 -0
  150. package/docs/talks3/user-04.md +3 -0
  151. package/docs/talks4/_body.txt +14 -0
  152. package/docs/talks4/_index.md +12 -0
  153. package/docs/talks4/_screenshot.png +0 -0
  154. package/docs/talks4/assistant-01.md +5 -0
  155. package/docs/talks4/assistant-02.md +5 -0
  156. package/docs/talks4/assistant-03.md +35 -0
  157. package/docs/talks4/assistant-04.md +512 -0
  158. package/docs/talks4/assistant-05.md +599 -0
  159. package/docs/talks4/user-01.md +3 -0
  160. package/docs/talks4/user-02.md +3 -0
  161. package/docs/talks4/user-03.md +88 -0
  162. package/docs/talks4/user-04.md +88 -0
  163. package/docs/talks4/user-05.md +7 -0
  164. package/docs/talks5/_body.txt +14 -0
  165. package/docs/talks5/_index.md +13 -0
  166. package/docs/talks5/_screenshot.png +0 -0
  167. package/docs/talks5/assistant-01.md +5 -0
  168. package/docs/talks5/assistant-02.md +5 -0
  169. package/docs/talks5/assistant-03.md +690 -0
  170. package/docs/talks5/assistant-04.md +758 -0
  171. package/docs/talks5/assistant-05.md +974 -0
  172. package/docs/talks5/user-01.md +3 -0
  173. package/docs/talks5/user-02.md +3 -0
  174. package/docs/talks5/user-03.md +105 -0
  175. package/docs/talks5/user-04.md +105 -0
  176. package/docs/talks5/user-05.md +63 -0
  177. package/docs/talks5/user-06.md +105 -0
  178. package/docs/talks6/_body.txt +14 -0
  179. package/docs/talks6/_index.md +9 -0
  180. package/docs/talks6/_screenshot.png +0 -0
  181. package/docs/talks6/assistant-01.md +5 -0
  182. package/docs/talks6/assistant-02.md +5 -0
  183. package/docs/talks6/assistant-03.md +1499 -0
  184. package/docs/talks6/user-01.md +3 -0
  185. package/docs/talks6/user-02.md +3 -0
  186. package/docs/talks6/user-03.md +88 -0
  187. package/docs/talks6/user-04.md +88 -0
  188. package/docs/talks7/_body.txt +14 -0
  189. package/docs/talks7/_index.md +10 -0
  190. package/docs/talks7/_screenshot.png +0 -0
  191. package/docs/talks7/assistant-01.md +5 -0
  192. package/docs/talks7/assistant-02.md +5 -0
  193. package/docs/talks7/assistant-03.md +523 -0
  194. package/docs/talks7/assistant-04.md +617 -0
  195. package/docs/talks7/user-01.md +3 -0
  196. package/docs/talks7/user-02.md +3 -0
  197. package/docs/talks7/user-03.md +105 -0
  198. package/docs/talks7/user-04.md +67 -0
  199. package/docs/talks8/conversa1.txt +1322 -0
  200. package/docs/talks8/conversa2.txt +237 -0
  201. package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +174 -0
  202. package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +59 -0
  203. package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +91 -0
  204. package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +116 -0
  205. package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +128 -0
  206. package/docs/talks9/README (2).md +489 -0
  207. package/docs/talks9/README.md +198 -0
  208. package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +80 -0
  209. package/docs/talks9/conversa.txt +544 -0
  210. package/docs/talks9/other (2).md +39 -0
  211. package/docs/talks9/other.md +57 -0
  212. package/docs/talks9/outro.txt +24 -0
  213. package/extension/README.md +3 -3
  214. package/extension/serviceworker.js +63 -4
  215. package/extension/worker.js +3 -1
  216. package/package.json +12 -2
  217. package/packager/manifest.js +1 -1
  218. package/scrape/agent.ts +122 -0
  219. package/scrape/batch.ts +79 -0
  220. package/scrape/biome.json +76 -0
  221. package/scrape/browser.ts +222 -0
  222. package/scrape/cache.ts +84 -0
  223. package/scrape/chunking.ts +193 -0
  224. package/scrape/cli.ts +105 -0
  225. package/scrape/crawler.ts +115 -0
  226. package/scrape/dev-server.ts +94 -0
  227. package/scrape/errors.ts +132 -0
  228. package/scrape/events.ts +26 -0
  229. package/scrape/extract.ts +165 -0
  230. package/scrape/fetch.ts +105 -0
  231. package/scrape/formats.ts +85 -0
  232. package/scrape/headers.ts +71 -0
  233. package/scrape/index.ts +92 -0
  234. package/scrape/jsdom.d.ts +6 -0
  235. package/scrape/llms-txt.ts +84 -0
  236. package/scrape/middleware.ts +90 -0
  237. package/scrape/package-lock.json +9397 -0
  238. package/scrape/package.json +1420 -0
  239. package/scrape/pool.ts +95 -0
  240. package/scrape/port.ts +18 -0
  241. package/scrape/proxy.ts +103 -0
  242. package/scrape/rate-limiter.ts +95 -0
  243. package/scrape/renderer.ts +194 -0
  244. package/scrape/retry.ts +64 -0
  245. package/scrape/robots.ts +137 -0
  246. package/scrape/scrape.ts +123 -0
  247. package/scrape/serialize.ts +310 -0
  248. package/scrape/server.ts +137 -0
  249. package/scrape/session.ts +109 -0
  250. package/scrape/sitemap.ts +131 -0
  251. package/scrape/tokens.ts +45 -0
  252. package/scrape/tsconfig.json +28 -0
  253. package/scrape/types.ts +214 -0
  254. package/scrape/utils.ts +77 -0
  255. package/scrape/vite.config.ts +55 -0
  256. package/scrape/vitest.config.ts +17 -0
  257. package/workflow/templates.js +4 -4
@@ -0,0 +1,1610 @@
1
+ # Pesquisa: Caching Strategies para Web Scraping (2026)
2
+
3
+ ## Table of Contents
4
+
5
+ - [Visão Geral](#visão-geral)
6
+ - [HTTP Response Caching](#http-response-caching)
7
+ - [In-Memory Caches](#in-memory-caches)
8
+ - [Abstraction Layers](#abstraction-layers)
9
+ - [Disk-Based Caching](#disk-based-caching)
10
+ - [TTL Strategies por Tipo de Conteúdo](#ttl-strategies-por-tipo-de-conteúdo)
11
+ - [Cache Invalidation](#cache-invalidation)
12
+ - [Crawlee Request Deduplication](#crawlee-request-deduplication)
13
+ - [Playwright Response Caching](#playwright-response-caching)
14
+ - [Multi-Layer Architecture](#multi-layer-architecture)
15
+ - [Código Completo](#código-completo)
16
+
17
+ ---
18
+
19
+ ## Visão Geral
20
+
21
+ Caching em web scraping serve para três propósitos principais:
22
+
23
+ 1. **Reduzir requests HTTP** — Evitar baixar o mesmo conteúdo repetidamente
24
+ 2. **Aumentar velocidade** — Respostas do cache são instantâneas vs. rede
25
+ 3. **Respeitar rate limits** — Menos requests = menos chance de bloqueio
26
+
27
+ ### Estratégias de Caching
28
+
29
+ | Nível | Armazenamento | Latência | Persistência | Caso de Uso |
30
+ |---|---|---|---|---|
31
+ | L1 | Memória (RAM) | <1ms | Volátil | Cache quente |
32
+ | L2 | Disco | 1-10ms | Persistente | Cache frio |
33
+ | HTTP | CDN/Proxy | Variável | Variável | Cache de rede |
34
+ | Browser | Playwright Page | N/A | Sessão | Cache de sessão |
35
+
36
+ ---
37
+
38
+ ## HTTP Response Caching
39
+
40
+ ### ETag / If-None-Match
41
+
42
+ O servidor retorna um `ETag` (hash do conteúdo). Na próxima request, o cliente envia `If-None-Match` com o ETag. Se o conteúdo não mudou, o servidor retorna `304 Not Modified` (sem corpo).
43
+
44
+ ```ts
45
+ import { gotScraping } from 'got-scraping';
46
+
47
+ class EtagCache {
48
+ private store = new Map<string, { etag: string; body: string; timestamp: number }>();
49
+
50
+ async fetch(url: string): Promise<{ body: string; fromCache: boolean }> {
51
+ const cached = this.store.get(url);
52
+
53
+ const headers: Record<string, string> = {};
54
+ if (cached?.etag) {
55
+ headers['If-None-Match'] = cached.etag;
56
+ }
57
+
58
+ const response = await gotScraping(url, {
59
+ headers,
60
+ throwHttpErrors: false,
61
+ });
62
+
63
+ if (response.statusCode === 304 && cached) {
64
+ console.log(`ETag HIT: ${url}`);
65
+ return { body: cached.body, fromCache: true };
66
+ }
67
+
68
+ const etag = response.headers['etag'];
69
+ if (etag) {
70
+ this.store.set(url, {
71
+ etag,
72
+ body: response.body,
73
+ timestamp: Date.now(),
74
+ });
75
+ }
76
+
77
+ console.log(`ETag MISS: ${url}`);
78
+ return { body: response.body, fromCache: false };
79
+ }
80
+ }
81
+
82
+ const cache = new EtagCache();
83
+ const result = await cache.fetch('https://example.com/api/data');
84
+ // Primeira request: MISS, baixa conteúdo, armazena ETag
85
+ // Segunda request: HIT, servidor retorna 304, usa cache local
86
+ ```
87
+
88
+ ### Last-Modified / If-Modified-Since
89
+
90
+ Similar ao ETag, mas usa timestamps. O servidor retorna `Last-Modified` e o cliente envia `If-Modified-Since`.
91
+
92
+ ```ts
93
+ import { gotScraping } from 'got-scraping';
94
+
95
+ class ModifiedCache {
96
+ private store = new Map<string, {
97
+ lastModified: string;
98
+ body: string;
99
+ timestamp: number;
100
+ }>();
101
+
102
+ async fetch(url: string): Promise<{ body: string; fromCache: boolean }> {
103
+ const cached = this.store.get(url);
104
+
105
+ const headers: Record<string, string> = {};
106
+ if (cached?.lastModified) {
107
+ headers['If-Modified-Since'] = cached.lastModified;
108
+ }
109
+
110
+ const response = await gotScraping(url, {
111
+ headers,
112
+ throwHttpErrors: false,
113
+ });
114
+
115
+ if (response.statusCode === 304 && cached) {
116
+ console.log(`Last-Modified HIT: ${url}`);
117
+ return { body: cached.body, fromCache: true };
118
+ }
119
+
120
+ const lastModified = response.headers['last-modified'];
121
+ if (lastModified) {
122
+ this.store.set(url, {
123
+ lastModified,
124
+ body: response.body,
125
+ timestamp: Date.now(),
126
+ });
127
+ }
128
+
129
+ console.log(`Last-Modified MISS: ${url}`);
130
+ return { body: response.body, fromCache: false };
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Cache-Control Directives
136
+
137
+ `Cache-Control` define regras de caching do lado do servidor:
138
+
139
+ | Diretiva | Significado |
140
+ |---|---|
141
+ | `max-age=N` | Cache válido por N segundos |
142
+ | `no-cache` | Revalidar antes de usar cache |
143
+ | `no-store` | Não armazenar em cache |
144
+ | `must-revalidate` | Revalidar quando expirar |
145
+ | `public` | Pode ser cacheado por CDNs |
146
+ | `private` | Só o cliente pode cachear |
147
+ | `s-maxage=N` | Max-age para shared caches (CDNs) |
148
+
149
+ ```ts
150
+ import { gotScraping } from 'got-scraping';
151
+
152
+ function parseCacheControl(header: string): Record<string, string | boolean> {
153
+ const directives: Record<string, string | boolean> = {};
154
+
155
+ for (const part of header.split(',')) {
156
+ const [key, value] = part.trim().split('=');
157
+ directives[key.trim()] = value?.trim() ?? true;
158
+ }
159
+
160
+ return directives;
161
+ }
162
+
163
+ function getCacheMaxAge(url: string, headers: Record<string, string>): number | null {
164
+ const cacheControl = headers['cache-control'];
165
+ if (!cacheControl) return null;
166
+
167
+ const directives = parseCacheControl(cacheControl);
168
+
169
+ if (directives['no-store'] || directives['no-cache']) {
170
+ return 0; // Não cachear
171
+ }
172
+
173
+ const maxAge = directives['max-age'] ?? directives['s-maxage'];
174
+ if (typeof maxAge === 'string') {
175
+ return Number.parseInt(maxAge, 10) * 1000; // Converter para ms
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ class SmartCache {
182
+ private store = new Map<string, { body: string; expiry: number }>();
183
+
184
+ async fetch(url: string): Promise<{ body: string; fromCache: boolean }> {
185
+ const cached = this.store.get(url);
186
+
187
+ // Verificar se cache ainda é válido
188
+ if (cached && cached.expiry > Date.now()) {
189
+ console.log(`Cache HIT: ${url}`);
190
+ return { body: cached.body, fromCache: true };
191
+ }
192
+
193
+ // Buscar novo conteúdo
194
+ const response = await gotScraping(url, { throwHttpErrors: false });
195
+
196
+ if (response.statusCode === 200) {
197
+ const maxAge = getCacheMaxAge(url, response.headers as Record<string, string>);
198
+
199
+ if (maxAge === null || maxAge > 0) {
200
+ this.store.set(url, {
201
+ body: response.body,
202
+ expiry: Date.now() + (maxAge ?? 300_000), // Default 5 min
203
+ });
204
+ }
205
+ }
206
+
207
+ console.log(`Cache MISS: ${url}`);
208
+ return { body: response.body, fromCache: false };
209
+ }
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## In-Memory Caches
216
+
217
+ ### lru-cache 11.x
218
+
219
+ `lru-cache` é a biblioteca de cache LRU mais popular no Node.js. Versão 11.x adiciona suporte a `fetchMethod` para cache transparente.
220
+
221
+ **Características:**
222
+ - LRU (Least Recently Used) eviction
223
+ - TTL (Time-To-Live) por item
224
+ - `fetchMethod` para cache transparente
225
+ - Max size com weighted size
226
+ - Stale-while-revalidate pattern
227
+
228
+ ```ts
229
+ import { LRUCache } from 'lru-cache';
230
+
231
+ // Cache básico
232
+ const cache = new LRUCache<string, string>({
233
+ max: 500, // Máximo 500 itens
234
+ ttl: 1000 * 60 * 5, // TTL de 5 minutos
235
+ });
236
+
237
+ cache.set('key1', 'value1');
238
+ const value = cache.get('key1');
239
+ // → 'value1'
240
+
241
+ // Cache com fetchMethod (transparente)
242
+ const apiCache = new LRUCache<string, string>({
243
+ max: 100,
244
+ ttl: 1000 * 60 * 10, // 10 minutos
245
+ fetchMethod: async (key) => {
246
+ console.log(`Buscando: ${key}`);
247
+ const response = await fetch(key);
248
+ return response.text();
249
+ },
250
+ });
251
+
252
+ // Se a chave existe, retorna do cache. Se não, busca e armazena.
253
+ const html = await apiCache.fetch('https://example.com');
254
+ // → Busca da web e armazena
255
+
256
+ const cached = await apiCache.fetch('https://example.com');
257
+ // → Retorna do cache (sem buscar)
258
+
259
+ // Cache com stale-while-revalidate
260
+ const swrCache = new LRUCache<string, string>({
261
+ max: 100,
262
+ ttl: 1000 * 60 * 5, // 5 min para fresh
263
+ allowStale: true, // Permitir stale
264
+ updateAgeOnGet: true, // Renovar TTL ao acessar
265
+ fetchMethod: async (key) => {
266
+ const response = await fetch(key);
267
+ return response.text();
268
+ },
269
+ });
270
+
271
+ // Retorna stale imediatamente, revalida em background
272
+ const html2 = await swrCache.fetch('https://example.com');
273
+ ```
274
+
275
+ ### node-cache 5.7.7
276
+
277
+ `node-cache` é uma alternativa simples com suporte nativo a stdTTL e eventos.
278
+
279
+ ```ts
280
+ import NodeCache from 'node-cache';
281
+
282
+ const cache = new NodeCache({
283
+ stdTTL: 300, // TTL padrão de 5 minutos
284
+ checkperiod: 60, // Verificar expirados a cada 60s
285
+ useClones: false, // Não clonar valores (performance)
286
+ });
287
+
288
+ // Set com TTL customizado
289
+ cache.set('key1', 'value1', 600); // 10 minutos
290
+ cache.set('key2', 'value2'); // TTL padrão (5 min)
291
+
292
+ // Get
293
+ const value = cache.get<string>('key1');
294
+
295
+ // Eventos
296
+ cache.on('expired', (key, value) => {
297
+ console.log(`Chave expirada: ${key}`);
298
+ });
299
+
300
+ cache.on('del', (key, value) => {
301
+ console.log(`Chave deletada: ${key}`);
302
+ });
303
+
304
+ // Stats
305
+ const stats = cache.getStats();
306
+ console.log(`Hits: ${stats.hits}, Misses: ${stats.misses}`);
307
+ ```
308
+
309
+ ### quick-lru 7.x
310
+
311
+ `quick-lru` é uma implementação LRU ultraleve e rápida. Sem TTL, sem features extras — apenas LRU puro.
312
+
313
+ ```ts
314
+ import QuickLRU from 'quick-lru';
315
+
316
+ const cache = new QuickLRU<string, string>({
317
+ maxSize: 1000,
318
+ });
319
+
320
+ cache.set('key1', 'value1');
321
+ cache.get('key1');
322
+ // → 'value1'
323
+
324
+ // Iterador
325
+ for (const [key, value] of cache) {
326
+ console.log(`${key}: ${value}`);
327
+ }
328
+
329
+ // Resizing
330
+ cache.resize(2000);
331
+ ```
332
+
333
+ ### Comparação de In-Memory Caches
334
+
335
+ | Feature | lru-cache 11.x | node-cache 5.7 | quick-lru 7.x |
336
+ |---|---|---|---|
337
+ | LRU | ✅ | ❌ (FIFO) | ✅ |
338
+ | TTL | ✅ | ✅ | ❌ |
339
+ | fetchMethod | ✅ | ❌ | ❌ |
340
+ | Eventos | ❌ | ✅ | ❌ |
341
+ | Tamanho | ~20KB | ~15KB | ~3KB |
342
+ | Performance | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
343
+ | Use case | Geral | Simples | Performance |
344
+
345
+ ---
346
+
347
+ ## Abstraction Layers
348
+
349
+ ### cache-manager 7.2.8
350
+
351
+ `cache-manager` abstrai o storage e permite trocar backends (memória, Redis, S3, etc.) sem mudar código.
352
+
353
+ **Características:**
354
+ - Multi-store (L1 + L2)
355
+ - Store switching (memória → Redis)
356
+ - Wrap pattern (cache transparente)
357
+ - Eventos
358
+
359
+ ```ts
360
+ import { caching } from 'cache-manager';
361
+
362
+ // Cache de memória
363
+ const memoryCache = await caching('memory', {
364
+ max: 1000,
365
+ ttl: 300, // 5 minutos
366
+ });
367
+
368
+ // Usar
369
+ await memoryCache.set('key1', 'value1', 600); // TTL de 10 min
370
+ const value = await memoryCache.get<string>('key1');
371
+
372
+ // Wrap — cache transparente
373
+ const data = await memoryCache.wrap('expensive-query', async () => {
374
+ // Esta função só executa se não houver cache
375
+ const result = await fetchExpensiveData();
376
+ return result;
377
+ }, { ttl: 600 });
378
+
379
+ // Store de Redis (se disponível)
380
+ // import { redisStore } from 'cache-manager-redis-yet';
381
+ // const redisCache = await caching(redisStore, {
382
+ // host: 'localhost',
383
+ // port: 6379,
384
+ // ttl: 300,
385
+ // });
386
+ ```
387
+
388
+ ### keyv 4.x
389
+
390
+ `keyv` é uma abstração simples com suporte a múltiplos backends via plugins.
391
+
392
+ ```ts
393
+ import Keyv from 'keyv';
394
+
395
+ // Cache de memória
396
+ const cache = new Keyv({ ttl: 300_000 }); // 5 min
397
+
398
+ // Usar
399
+ await cache.set('key1', 'value1');
400
+ const value = await cache.get('key1');
401
+
402
+ // Com Redis (se disponível)
403
+ // import KeyvRedis from '@keyv/redis';
404
+ // const cache = new Keyv(new KeyvRedis('redis://localhost:6379'));
405
+
406
+ // Namespace
407
+ const userCache = new Keyv({ namespace: 'users' });
408
+ const productCache = new Keyv({ namespace: 'products' });
409
+
410
+ await userCache.set('user:123', { name: 'João' });
411
+ await productCache.set('product:456', { name: 'Produto' });
412
+
413
+ // Iteração
414
+ for await (const [key, value] of cache.iterator()) {
415
+ console.log(`${key}: ${value}`);
416
+ }
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Disk-Based Caching
422
+
423
+ ### cacache 21.0.0
424
+
425
+ `cacache` é um cache content-addressable (baseado em hash do conteúdo). Usado pelo npm internamente.
426
+
427
+ **Características:**
428
+ - Content-addressable (hash SHA-512 do conteúdo)
429
+ - Integrity verification automática
430
+ - Suporte a metadados
431
+ - Compactação
432
+ - Não expira por padrão (usar cleanup manual)
433
+
434
+ ```ts
435
+ import cacache from 'cacache';
436
+ import { tmpdir } from 'os';
437
+ import { join } from 'path';
438
+
439
+ const CACHE_DIR = join(tmpdir(), 'my-scraper-cache');
440
+
441
+ // Armazenar
442
+ const data = { url: 'https://example.com', content: '<html>...</html>' };
443
+ await cacache.put(CACHE_DIR, 'https://example.com', JSON.stringify(data));
444
+
445
+ // Recuperar
446
+ const cached = await cacache.get(CACHE_DIR, 'https://example.com');
447
+ if (cached) {
448
+ const parsed = JSON.parse(cached.data.toString());
449
+ console.log('Cache HIT:', parsed.url);
450
+ }
451
+
452
+ // Verificar se existe
453
+ const hasKey = await cacache.get(CACHE_DIR, 'https://example.com');
454
+ console.log('Existe:', !!hasKey);
455
+
456
+ // Deletar
457
+ await cacache.rm(CACHE_DIR, 'https://example.com');
458
+
459
+ // Listar todas as chaves
460
+ const entries = await cacache.ls(CACHE_DIR);
461
+ for (const [key, entry] of Object.entries(entries)) {
462
+ console.log(`${key}: ${entry.integrity}`);
463
+ }
464
+
465
+ // Cleanup de entradas antigas
466
+ const { entries: allEntries } = await cacache.get(CACHE_DIR);
467
+ ```
468
+
469
+ ### flat-cache 6.1.22
470
+
471
+ `flat-cache` armazena tudo em um único arquivo JSON. Simples e rápido para caches pequenos.
472
+
473
+ ```ts
474
+ import flatCache from 'flat-cache';
475
+ import { join } from 'path';
476
+
477
+ const cacheDir = join(process.cwd(), '.cache');
478
+ const cache = flatCache.load('scraper-cache', cacheDir);
479
+
480
+ // Set
481
+ cache.set('https://example.com', {
482
+ content: '<html>...</html>',
483
+ timestamp: Date.now(),
484
+ });
485
+ cache.save(true); // true = pretty print
486
+
487
+ // Get
488
+ const cached = cache.getKey('https://example.com');
489
+ console.log(cached);
490
+
491
+ // Deletar
492
+ cache.removeKey('https://example.com');
493
+
494
+ // Listar
495
+ const keys = cache.allKeys();
496
+ console.log(keys);
497
+
498
+ // Cleanup de itens antigos
499
+ const allEntries = cache.all();
500
+ const oneHourAgo = Date.now() - 3600_000;
501
+
502
+ for (const [key, entry] of Object.entries(allEntries)) {
503
+ if ((entry as any).timestamp < oneHourAgo) {
504
+ cache.removeKey(key);
505
+ }
506
+ }
507
+ cache.save();
508
+ ```
509
+
510
+ ### Comparação de Disk-Based Caches
511
+
512
+ | Feature | cacache 21.0 | flat-cache 6.1 |
513
+ |---|---|---|
514
+ | Armazenamento | Content-addressable | JSON file |
515
+ | Integridade | SHA-512 automática | Nenhuma |
516
+ | Performance | ⭐⭐⭐ | ⭐⭐⭐⭐ |
517
+ | Tamanho | Grande (hashes) | Pequeno |
518
+ | Use case | Cache robusto | Cache simples |
519
+ | Cleanup | Manual | Manual |
520
+
521
+ ---
522
+
523
+ ## TTL Strategies por Tipo de Conteúdo
524
+
525
+ ### Estimativas de TTL
526
+
527
+ | Tipo de Conteúdo | TTL | Exemplo |
528
+ |---|---|---|
529
+ | **Estático** | 24-72 horas | Páginas de termos, sobre, FAQ |
530
+ | **Semi-estático** | 4-24 horas | Blog posts, documentação |
531
+ | **Dinâmico** | 30 min - 4 horas | Páginas de produto, preços |
532
+ | **Tempo real** | 30 segundos - 5 minutos | Busca, feed, cotações |
533
+ | **Transacional** | 0 (não cachear) | Checkout, login, pagamento |
534
+
535
+ ### Implementação por Tipo
536
+
537
+ ```ts
538
+ import { LRUCache } from 'lru-cache';
539
+
540
+ enum ContentType {
541
+ STATIC = 'static',
542
+ SEMI_STATIC = 'semi-static',
543
+ DYNAMIC = 'dynamic',
544
+ REALTIME = 'realtime',
545
+ TRANSACTIONAL = 'transactional',
546
+ }
547
+
548
+ const TTL_CONFIG: Record<ContentType, { ttl: number; staleWhileRevalidate?: number }> = {
549
+ [ContentType.STATIC]: {
550
+ ttl: 1000 * 60 * 60 * 24, // 24 horas
551
+ staleWhileRevalidate: 1000 * 60 * 60 * 48, // 48h stale
552
+ },
553
+ [ContentType.SEMI_STATIC]: {
554
+ ttl: 1000 * 60 * 60 * 4, // 4 horas
555
+ staleWhileRevalidate: 1000 * 60 * 60 * 12, // 12h stale
556
+ },
557
+ [ContentType.DYNAMIC]: {
558
+ ttl: 1000 * 60 * 30, // 30 minutos
559
+ staleWhileRevalidate: 1000 * 60 * 60, // 1h stale
560
+ },
561
+ [ContentType.REALTIME]: {
562
+ ttl: 1000 * 30, // 30 segundos
563
+ staleWhileRevalidate: 1000 * 60, // 1 minuto stale
564
+ },
565
+ [ContentType.TRANSACTIONAL]: {
566
+ ttl: 0, // Não cachear
567
+ },
568
+ };
569
+
570
+ function classifyContentType(url: string): ContentType {
571
+ const parsed = new URL(url);
572
+ const path = parsed.pathname.toLowerCase();
573
+
574
+ if (path.match(/\.(css|js|png|jpg|gif|svg|ico|woff2?)$/)) {
575
+ return ContentType.STATIC;
576
+ }
577
+
578
+ if (path.startsWith('/terms') || path.startsWith('/privacy') || path.startsWith('/about')) {
579
+ return ContentType.STATIC;
580
+ }
581
+
582
+ if (path.startsWith('/blog/') || path.startsWith('/docs/')) {
583
+ return ContentType.SEMI_STATIC;
584
+ }
585
+
586
+ if (path.startsWith('/products/') || path.startsWith('/categories/')) {
587
+ return ContentType.DYNAMIC;
588
+ }
589
+
590
+ if (path.startsWith('/search') || path.startsWith('/api/search')) {
591
+ return ContentType.REALTIME;
592
+ }
593
+
594
+ if (path.startsWith('/checkout') || path.startsWith('/cart')) {
595
+ return ContentType.TRANSACTIONAL;
596
+ }
597
+
598
+ return ContentType.DYNAMIC;
599
+ }
600
+
601
+ // Cache com TTL dinâmico
602
+ class DynamicTTLCache {
603
+ private cache = new LRUCache<string, { body: string; type: ContentType }>({
604
+ max: 1000,
605
+ });
606
+
607
+ async fetch(url: string, fetcher: () => Promise<string>): Promise<string> {
608
+ const type = classifyContentType(url);
609
+ const config = TTL_CONFIG[type];
610
+
611
+ if (config.ttl === 0) {
612
+ // Não cachear
613
+ return fetcher();
614
+ }
615
+
616
+ const cached = this.cache.get(url);
617
+ if (cached && cached.type === type) {
618
+ return cached.body;
619
+ }
620
+
621
+ const body = await fetcher();
622
+ this.cache.set(url, { body, type }, { ttl: config.ttl });
623
+
624
+ return body;
625
+ }
626
+ }
627
+ ```
628
+
629
+ ### Detecção de Tipo via Headers
630
+
631
+ ```ts
632
+ function detectContentType(headers: Record<string, string>): ContentType {
633
+ const contentType = headers['content-type'] ?? '';
634
+ const cacheControl = headers['cache-control'] ?? '';
635
+
636
+ // Se servidor diz "no-store", não cachear
637
+ if (cacheControl.includes('no-store')) {
638
+ return ContentType.TRANSACTIONAL;
639
+ }
640
+
641
+ // Se servidor diz "max-age", usar esse valor
642
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
643
+ if (maxAgeMatch) {
644
+ const maxAge = Number.parseInt(maxAgeMatch[1], 10);
645
+ if (maxAge <= 60) return ContentType.REALTIME;
646
+ if (maxAge <= 3600) return ContentType.DYNAMIC;
647
+ if (maxAge <= 86400) return ContentType.SEMI_STATIC;
648
+ return ContentType.STATIC;
649
+ }
650
+
651
+ // Classificar por content-type
652
+ if (contentType.includes('json')) return ContentType.DYNAMIC;
653
+ if (contentType.includes('html')) return ContentType.SEMI_STATIC;
654
+ if (contentType.includes('image') || contentType.includes('css') || contentType.includes('js')) {
655
+ return ContentType.STATIC;
656
+ }
657
+
658
+ return ContentType.DYNAMIC;
659
+ }
660
+ ```
661
+
662
+ ---
663
+
664
+ ## Cache Invalidation
665
+
666
+ ### TTL-Based
667
+
668
+ O formato mais simples: cada item tem um TTL e é automaticamente removido quando expira.
669
+
670
+ ```ts
671
+ import { LRUCache } from 'lru-cache';
672
+
673
+ const cache = new LRUCache<string, string>({
674
+ max: 1000,
675
+ ttl: 1000 * 60 * 5, // 5 minutos
676
+ });
677
+
678
+ // Item expira automaticamente após 5 minutos
679
+ cache.set('key', 'value');
680
+ ```
681
+
682
+ ### Explicit Invalidation
683
+
684
+ Delete manualmente quando o conteúdo muda.
685
+
686
+ ```ts
687
+ class ExplicitCache {
688
+ private cache = new Map<string, { body: string; version: number }>();
689
+
690
+ set(url: string, body: string): void {
691
+ const existing = this.cache.get(url);
692
+ this.cache.set(url, {
693
+ body,
694
+ version: (existing?.version ?? 0) + 1,
695
+ });
696
+ }
697
+
698
+ get(url: string): string | null {
699
+ return this.cache.get(url)?.body ?? null;
700
+ }
701
+
702
+ invalidate(url: string): void {
703
+ this.cache.delete(url);
704
+ }
705
+
706
+ invalidatePattern(pattern: RegExp): number {
707
+ let count = 0;
708
+ for (const key of this.cache.keys()) {
709
+ if (pattern.test(key)) {
710
+ this.cache.delete(key);
711
+ count++;
712
+ }
713
+ }
714
+ return count;
715
+ }
716
+
717
+ invalidateAll(): void {
718
+ this.cache.clear();
719
+ }
720
+ }
721
+
722
+ const cache = new ExplicitCache();
723
+ cache.set('https://example.com/page1', 'content1');
724
+
725
+ // Invalidar uma URL
726
+ cache.invalidate('https://example.com/page1');
727
+
728
+ // Invalidar todas as URLs de um domínio
729
+ cache.invalidatePattern(/^https:\/\/example\.com\//);
730
+ ```
731
+
732
+ ### Versioned Keys
733
+
734
+ Cada versão do conteúdo tem uma chave diferente. Cache antigo continua válido até expirar.
735
+
736
+ ```ts
737
+ class VersionedCache {
738
+ private cache = new Map<string, { body: string; version: number }>();
739
+ private versions = new Map<string, number>();
740
+
741
+ getCurrentVersion(url: string): number {
742
+ return this.versions.get(url) ?? 0;
743
+ }
744
+
745
+ set(url: string, body: string): void {
746
+ const version = this.getCurrentVersion(url) + 1;
747
+ this.versions.set(url, version);
748
+ this.cache.set(`${url}#v${version}`, { body, version });
749
+ }
750
+
751
+ get(url: string): string | null {
752
+ const version = this.getCurrentVersion(url);
753
+ if (version === 0) return null;
754
+
755
+ const cached = this.cache.get(`${url}#v${version}`);
756
+ return cached?.body ?? null;
757
+ }
758
+
759
+ // Versão específica
760
+ getAtVersion(url: string, version: number): string | null {
761
+ const cached = this.cache.get(`${url}#v${version}`);
762
+ return cached?.body ?? null;
763
+ }
764
+
765
+ // Limpar versões antigas (manter últimas N)
766
+ cleanup(url: string, keepLast: number = 3): void {
767
+ const currentVersion = this.getCurrentVersion(url);
768
+ const versionsToDelete = currentVersion - keepLast;
769
+
770
+ for (let v = 1; v <= versionsToDelete; v++) {
771
+ this.cache.delete(`${url}#v${v}`);
772
+ }
773
+ }
774
+ }
775
+
776
+ const cache = new VersionedCache();
777
+ cache.set('https://example.com', 'v1');
778
+ cache.set('https://example.com', 'v2');
779
+ cache.set('https://example.com', 'v3');
780
+
781
+ console.log(cache.get('https://example.com')); // v3
782
+ console.log(cache.getAtVersion('https://example.com', 1)); // v1
783
+ ```
784
+
785
+ ### Pattern-Based Invalidation
786
+
787
+ Invalidar múltiplas chaves que correspondem a um padrão.
788
+
789
+ ```ts
790
+ class PatternCache {
791
+ private cache = new Map<string, string>();
792
+
793
+ set(key: string, value: string): void {
794
+ this.cache.set(key, value);
795
+ }
796
+
797
+ get(key: string): string | null {
798
+ return this.cache.get(key) ?? null;
799
+ }
800
+
801
+ invalidate(pattern: string | RegExp): string[] {
802
+ const deleted: string[] = [];
803
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
804
+
805
+ for (const key of this.cache.keys()) {
806
+ if (regex.test(key)) {
807
+ this.cache.delete(key);
808
+ deleted.push(key);
809
+ }
810
+ }
811
+
812
+ return deleted;
813
+ }
814
+
815
+ // Invalidar por domínio
816
+ invalidateDomain(domain: string): string[] {
817
+ return this.invalidate(new RegExp(`^https?://${domain.replace('.', '\\.')}/`));
818
+ }
819
+
820
+ // Invalidar por path
821
+ invalidatePath(path: string): string[] {
822
+ return this.invalidate(new RegExp(`${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
823
+ }
824
+ }
825
+
826
+ const cache = new PatternCache();
827
+ cache.set('https://example.com/page1', 'c1');
828
+ cache.set('https://example.com/page2', 'c2');
829
+ cache.set('https://other.com/page1', 'c3');
830
+
831
+ const deleted = cache.invalidateDomain('example.com');
832
+ // → ['https://example.com/page1', 'https://example.com/page2']
833
+ ```
834
+
835
+ ### Event-Driven Invalidation
836
+
837
+ Invalidar cache quando um evento externo ocorre.
838
+
839
+ ```ts
840
+ import { EventEmitter } from 'events';
841
+
842
+ class EventDrivenCache extends EventEmitter {
843
+ private cache = new Map<string, { body: string; tags: string[] }>();
844
+
845
+ set(url: string, body: string, tags: string[] = []): void {
846
+ this.cache.set(url, { body, tags });
847
+ }
848
+
849
+ get(url: string): string | null {
850
+ return this.cache.get(url)?.body ?? null;
851
+ }
852
+
853
+ // Invalidar por tag
854
+ invalidateTag(tag: string): string[] {
855
+ const deleted: string[] = [];
856
+
857
+ for (const [key, entry] of this.cache.entries()) {
858
+ if (entry.tags.includes(tag)) {
859
+ this.cache.delete(key);
860
+ deleted.push(key);
861
+ }
862
+ }
863
+
864
+ this.emit('invalidated', { tag, count: deleted.length });
865
+ return deleted;
866
+ }
867
+
868
+ // Invalidar quando conteúdo muda
869
+ onContentChange(url: string, tags: string[]): void {
870
+ // Invalidar todas as entradas com tags relacionadas
871
+ for (const tag of tags) {
872
+ this.invalidateTag(tag);
873
+ }
874
+ }
875
+ }
876
+
877
+ const cache = new EventDrivenCache();
878
+ cache.on('invalidated', ({ tag, count }) => {
879
+ console.log(`Tag "${tag}" invalidou ${count} entradas`);
880
+ });
881
+
882
+ cache.set('https://example.com/products', 'products-list', ['products', 'homepage']);
883
+ cache.set('https://example.com/product/1', 'product-1', ['products', 'product-1']);
884
+ cache.set('https://example.com/blog', 'blog-list', ['blog', 'homepage']);
885
+
886
+ cache.invalidateTag('products');
887
+ // → Invalidou 2 entradas
888
+ ```
889
+
890
+ ---
891
+
892
+ ## Crawlee Request Deduplication
893
+
894
+ ### uniqueKey
895
+
896
+ Crawlee usa `uniqueKey` para deduplicação automática de requests. Cada request tem uma chave única derivada da URL e opções.
897
+
898
+ ```ts
899
+ import { PlaywrightCrawler } from 'crawlee';
900
+
901
+ const crawler = new PlaywrightCrawler({
902
+ requestHandlerTimeoutSecs: 30,
903
+ async requestHandler({ page, enqueueLinks, request }) {
904
+ // Crawlee automaticamente ignora URLs duplicadas
905
+ // baseado no uniqueKey
906
+
907
+ const links = await page.$$eval('a[href]', anchors =>
908
+ anchors.map(a => a.href)
909
+ );
910
+
911
+ // enqueueLinks deduplica automaticamente
912
+ await enqueueLinks({ urls: links });
913
+ },
914
+ });
915
+
916
+ // Crawlee mantém um set de uniqueKeys em memória e disco
917
+ // URLs com o mesmo uniqueKey são ignoradas
918
+ ```
919
+
920
+ ### customKeyFunction
921
+
922
+ Para customizar a deduplicação (ex: ignorar query params):
923
+
924
+ ```ts
925
+ import { PlaywrightCrawler, Request } from 'crawlee';
926
+
927
+ const crawler = new PlaywrightCrawler({
928
+ requestHandlerTimeoutSecs: 30,
929
+ async requestHandler({ page, enqueueLinks }) {
930
+ const links = await page.$$eval('a[href]', anchors =>
931
+ anchors.map(a => a.href)
932
+ );
933
+
934
+ // Criar requests com chave customizada
935
+ for (const url of links) {
936
+ const request = new Request({
937
+ url,
938
+ // Crawlee usa URL como uniqueKey por padrão
939
+ // Para customizar, usar手動 construction
940
+ });
941
+ }
942
+
943
+ // Ou usar glob patterns no enqueueLinks
944
+ await enqueueLinks({
945
+ globs: ['https://example.com/**'],
946
+ exclude: ['**/page=*'], // Excluir paginação
947
+ });
948
+ },
949
+ });
950
+ ```
951
+
952
+ ### Request Queue Persistence
953
+
954
+ O `RequestQueue` persiste em disco (SQLite via `@crawlee/storage`), sobrevivendo a crashes:
955
+
956
+ ```ts
957
+ import { RequestQueue, PlaywrightCrawler } from 'crawlee';
958
+
959
+ // Abrir fila persistente
960
+ const queue = await RequestQueue.open('my-crawl-queue');
961
+
962
+ // Adicionar request
963
+ await queue.addRequest({
964
+ url: 'https://example.com',
965
+ userData: { depth: 0 },
966
+ });
967
+
968
+ // Abrir crawler com fila existente
969
+ const crawler = new PlaywrightCrawler({
970
+ requestQueue: queue,
971
+ async requestHandler({ page, request }) {
972
+ // Processar...
973
+ },
974
+ });
975
+
976
+ // Mesmo após crash, a fila continua de onde parou
977
+ await crawler.run(); // Retoma automaticamente
978
+ ```
979
+
980
+ ---
981
+
982
+ ## Playwright Response Caching
983
+
984
+ ### page.route() Interception
985
+
986
+ Interceptar responses no Playwright para cache de sessão:
987
+
988
+ ```ts
989
+ import { chromium } from 'playwright';
990
+
991
+ class PlaywrightCache {
992
+ private cache = new Map<string, { body: string; headers: Record<string, string> }>();
993
+
994
+ async setupCache(page: any): Promise<void> {
995
+ await page.route('**/*', async (route: any) => {
996
+ const url = route.request().url();
997
+ const cached = this.cache.get(url);
998
+
999
+ if (cached) {
1000
+ console.log(`Playwright cache HIT: ${url}`);
1001
+ await route.fulfill({
1002
+ status: 200,
1003
+ contentType: 'text/html',
1004
+ body: cached.body,
1005
+ });
1006
+ return;
1007
+ }
1008
+
1009
+ // Continuar request normalmente
1010
+ const response = await route.fetch();
1011
+ const body = await response.text();
1012
+
1013
+ // Armazenar em cache
1014
+ this.cache.set(url, {
1015
+ body,
1016
+ headers: response.headers(),
1017
+ });
1018
+
1019
+ await route.fulfill({
1020
+ status: response.status(),
1021
+ headers: response.headers(),
1022
+ body,
1023
+ });
1024
+ });
1025
+ }
1026
+ }
1027
+
1028
+ // Usar
1029
+ const browser = await chromium.launch();
1030
+ const context = await browser.newContext();
1031
+ const page = await context.newPage();
1032
+
1033
+ const cache = new PlaywrightCache();
1034
+ await cache.setupCache(page);
1035
+
1036
+ // Primeira visita: baixa da web
1037
+ await page.goto('https://example.com');
1038
+ // Segunda visita: usa cache
1039
+ await page.goto('https://example.com');
1040
+ ```
1041
+
1042
+ ### Cache com Estratégia de Revalidação
1043
+
1044
+ ```ts
1045
+ import { chromium } from 'playwright';
1046
+
1047
+ class RevalidatingCache {
1048
+ private cache = new Map<string, {
1049
+ body: string;
1050
+ etag?: string;
1051
+ lastModified?: string;
1052
+ expiry: number;
1053
+ }>();
1054
+
1055
+ async setupCache(page: any, defaultTtl = 300_000): Promise<void> {
1056
+ await page.route('**/*', async (route: any) => {
1057
+ const url = route.request().url();
1058
+ const cached = this.cache.get(url);
1059
+
1060
+ // Verificar se cache ainda é válido
1061
+ if (cached && cached.expiry > Date.now()) {
1062
+ console.log(`Cache HIT: ${url}`);
1063
+ await route.fulfill({
1064
+ status: 200,
1065
+ body: cached.body,
1066
+ });
1067
+ return;
1068
+ }
1069
+
1070
+ // Cache expirado ou inexistente — buscar com headers de revalidação
1071
+ const headers: Record<string, string> = {};
1072
+ if (cached?.etag) headers['If-None-Match'] = cached.etag;
1073
+ if (cached?.lastModified) headers['If-Modified-Since'] = cached.lastModified;
1074
+
1075
+ const response = await route.fetch({ headers });
1076
+
1077
+ if (response.status() === 304 && cached) {
1078
+ // Conteúdo não mudou — renovar TTL
1079
+ console.log(`Revalidado: ${url}`);
1080
+ this.cache.set(url, {
1081
+ ...cached,
1082
+ expiry: Date.now() + defaultTtl,
1083
+ });
1084
+
1085
+ await route.fulfill({
1086
+ status: 200,
1087
+ body: cached.body,
1088
+ });
1089
+ return;
1090
+ }
1091
+
1092
+ // Conteúdo mudou — atualizar cache
1093
+ const body = await response.text();
1094
+ const responseHeaders = response.headers();
1095
+
1096
+ this.cache.set(url, {
1097
+ body,
1098
+ etag: responseHeaders['etag'] ?? cached?.etag,
1099
+ lastModified: responseHeaders['last-modified'] ?? cached?.lastModified,
1100
+ expiry: Date.now() + defaultTtl,
1101
+ });
1102
+
1103
+ console.log(`Cache MISS: ${url}`);
1104
+ await route.fulfill({
1105
+ status: response.status(),
1106
+ headers: responseHeaders,
1107
+ body,
1108
+ });
1109
+ });
1110
+ }
1111
+ }
1112
+ ```
1113
+
1114
+ ### Cache Seletivo (Apenas para Tipos Específicos)
1115
+
1116
+ ```ts
1117
+ class SelectiveCache {
1118
+ private cache = new Map<string, string>();
1119
+
1120
+ async setupCache(page: any): Promise<void> {
1121
+ await page.route('**/*', async (route: any) => {
1122
+ const url = route.request().url();
1123
+ const resourceType = route.request().resourceType();
1124
+
1125
+ // Cache apenas para CSS, JS e imagens
1126
+ if (!['stylesheet', 'script', 'image', 'font'].includes(resourceType)) {
1127
+ await route.continue();
1128
+ return;
1129
+ }
1130
+
1131
+ const cached = this.cache.get(url);
1132
+ if (cached) {
1133
+ console.log(`Cache HIT (${resourceType}): ${url}`);
1134
+ await route.fulfill({ body: cached });
1135
+ return;
1136
+ }
1137
+
1138
+ const response = await route.fetch();
1139
+ const body = await response.text();
1140
+
1141
+ if (response.status() === 200) {
1142
+ this.cache.set(url, body);
1143
+ }
1144
+
1145
+ await route.fulfill({
1146
+ status: response.status(),
1147
+ body,
1148
+ });
1149
+ });
1150
+ }
1151
+ }
1152
+ ```
1153
+
1154
+ ---
1155
+
1156
+ ## Multi-Layer Architecture
1157
+
1158
+ ### L1 (Memória) + L2 (Disco)
1159
+
1160
+ Arquitetura de duas camadas: cache quente em memória, cache frio em disco.
1161
+
1162
+ ```ts
1163
+ import { LRUCache } from 'lru-cache';
1164
+ import cacache from 'cacache';
1165
+ import { join } from 'path';
1166
+ import { tmpdir } from 'os';
1167
+
1168
+ class MultiLayerCache {
1169
+ private l1: LRUCache<string, string>;
1170
+ private l2Dir: string;
1171
+
1172
+ constructor(l2Dir?: string) {
1173
+ this.l1 = new LRUCache<string, string>({
1174
+ max: 500,
1175
+ ttl: 1000 * 60 * 5, // 5 minutos
1176
+ });
1177
+
1178
+ this.l2Dir = l2Dir ?? join(tmpdir(), 'scraper-cache');
1179
+ }
1180
+
1181
+ async get(url: string): Promise<{ body: string; source: 'l1' | 'l2' | null }> {
1182
+ // L1: Memória
1183
+ const l1Value = this.l1.get(url);
1184
+ if (l1Value) {
1185
+ return { body: l1Value, source: 'l1' };
1186
+ }
1187
+
1188
+ // L2: Disco
1189
+ try {
1190
+ const l2Entry = await cacache.get(this.l2Dir, url);
1191
+ if (l2Entry) {
1192
+ const body = l2Entry.data.toString();
1193
+ // Promover para L1
1194
+ this.l1.set(url, body);
1195
+ return { body, source: 'l2' };
1196
+ }
1197
+ } catch {
1198
+ // Cache miss no disco
1199
+ }
1200
+
1201
+ return { body: '', source: null };
1202
+ }
1203
+
1204
+ async set(url: string, body: string): Promise<void> {
1205
+ // Armazenar em L1
1206
+ this.l1.set(url, body);
1207
+
1208
+ // Armazenar em L2
1209
+ await cacache.put(this.l2Dir, url, body, {
1210
+ metadata: { url, timestamp: Date.now() },
1211
+ });
1212
+ }
1213
+
1214
+ async invalidate(url: string): Promise<void> {
1215
+ this.l1.delete(url);
1216
+ await cacache.rm(this.l2Dir, url);
1217
+ }
1218
+
1219
+ async invalidateDomain(domain: string): Promise<void> {
1220
+ // L1: invalidar todas as chaves do domínio
1221
+ for (const key of this.l1.keys()) {
1222
+ if (key.includes(domain)) {
1223
+ this.l1.delete(key);
1224
+ }
1225
+ }
1226
+
1227
+ // L2: listar e deletar
1228
+ const entries = await cacache.ls(this.l2Dir);
1229
+ for (const [key] of Object.entries(entries)) {
1230
+ if (key.includes(domain)) {
1231
+ await cacache.rm(this.l2Dir, key);
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ async stats(): Promise<{ l1Size: number; l2Size: number }> {
1237
+ const entries = await cacache.ls(this.l2Dir);
1238
+ return {
1239
+ l1Size: this.l1.size,
1240
+ l2Size: Object.keys(entries).length,
1241
+ };
1242
+ }
1243
+ }
1244
+
1245
+ // Usar
1246
+ const cache = new MultiLayerCache();
1247
+
1248
+ // Fetch com cache de duas camadas
1249
+ async function fetchWithCache(url: string): Promise<string> {
1250
+ const cached = await cache.get(url);
1251
+ if (cached.source) {
1252
+ console.log(`Cache HIT (${cached.source}): ${url}`);
1253
+ return cached.body;
1254
+ }
1255
+
1256
+ console.log(`Cache MISS: ${url}`);
1257
+ const response = await fetch(url);
1258
+ const body = await response.text();
1259
+
1260
+ await cache.set(url, body);
1261
+ return body;
1262
+ }
1263
+ ```
1264
+
1265
+ ### L1 + L2 com Stale-While-Revalidate
1266
+
1267
+ ```ts
1268
+ class SWRCache {
1269
+ private l1: LRUCache<string, { body: string; expiry: number }>;
1270
+ private l2Dir: string;
1271
+
1272
+ constructor(l2Dir: string) {
1273
+ this.l1 = new LRUCache<string, { body: string; expiry: number }>({
1274
+ max: 500,
1275
+ });
1276
+ this.l2Dir = l2Dir;
1277
+ }
1278
+
1279
+ async fetch(
1280
+ url: string,
1281
+ fetcher: () => Promise<string>,
1282
+ options: { ttl?: number; swr?: number } = {},
1283
+ ): Promise<string> {
1284
+ const { ttl = 300_000, swr = 600_000 } = options; // 5 min TTL, 10 min SWR
1285
+ const now = Date.now();
1286
+
1287
+ // L1 check
1288
+ const l1 = this.l1.get(url);
1289
+ if (l1 && l1.expiry > now) {
1290
+ return l1.body; // Fresh
1291
+ }
1292
+
1293
+ if (l1 && l1.expiry + swr > now) {
1294
+ // Stale mas utilizável — revalidar em background
1295
+ console.log(`SWR: Revalidating ${url} in background`);
1296
+ fetcher().then(body => {
1297
+ this.l1.set(url, { body, expiry: now + ttl });
1298
+ cacache.put(this.l2Dir, url, body);
1299
+ });
1300
+ return l1.body; // Return stale imediatamente
1301
+ }
1302
+
1303
+ // L2 check
1304
+ try {
1305
+ const l2 = await cacache.get(this.l2Dir, url);
1306
+ if (l2) {
1307
+ const body = l2.data.toString();
1308
+ this.l1.set(url, { body, expiry: now + ttl });
1309
+ return body;
1310
+ }
1311
+ } catch {
1312
+ // L2 miss
1313
+ }
1314
+
1315
+ // Fetch completo
1316
+ console.log(`Full fetch: ${url}`);
1317
+ const body = await fetcher();
1318
+
1319
+ this.l1.set(url, { body, expiry: now + ttl });
1320
+ await cacache.put(this.l2Dir, url, body);
1321
+
1322
+ return body;
1323
+ }
1324
+ }
1325
+ ```
1326
+
1327
+ ---
1328
+
1329
+ ## Código Completo
1330
+
1331
+ ### Exemplo 1: Web Scraper com Cache Completo
1332
+
1333
+ ```ts
1334
+ import { PlaywrightCrawler } from 'crawlee';
1335
+ import { LRUCache } from 'lru-cache';
1336
+ import cacache from 'cacache';
1337
+ import { join } from 'path';
1338
+ import { tmpdir } from 'os';
1339
+
1340
+ interface ScrapeCacheConfig {
1341
+ l1MaxSize: number;
1342
+ l1Ttl: number;
1343
+ l2Dir: string;
1344
+ defaultTtl: number;
1345
+ }
1346
+
1347
+ class ScrapeCache {
1348
+ private l1: LRUCache<string, string>;
1349
+ private l2Dir: string;
1350
+
1351
+ constructor(config: ScrapeCacheConfig) {
1352
+ this.l1 = new LRUCache({
1353
+ max: config.l1MaxSize,
1354
+ ttl: config.l1Ttl,
1355
+ });
1356
+ this.l2Dir = config.l2Dir;
1357
+ }
1358
+
1359
+ async get(url: string): Promise<string | null> {
1360
+ const l1 = this.l1.get(url);
1361
+ if (l1) return l1;
1362
+
1363
+ try {
1364
+ const l2 = await cacache.get(this.l2Dir, url);
1365
+ if (l2) {
1366
+ const body = l2.data.toString();
1367
+ this.l1.set(url, body);
1368
+ return body;
1369
+ }
1370
+ } catch {}
1371
+
1372
+ return null;
1373
+ }
1374
+
1375
+ async set(url: string, body: string): Promise<void> {
1376
+ this.l1.set(url, body);
1377
+ await cacache.put(this.l2Dir, url, body, {
1378
+ metadata: { url, timestamp: Date.now() },
1379
+ });
1380
+ }
1381
+
1382
+ async size(): Promise<number> {
1383
+ const entries = await cacache.ls(this.l2Dir);
1384
+ return Object.keys(entries).length;
1385
+ }
1386
+ }
1387
+
1388
+ // Configurar
1389
+ const cache = new ScrapeCache({
1390
+ l1MaxSize: 500,
1391
+ l1Ttl: 1000 * 60 * 5,
1392
+ l2Dir: join(tmpdir(), 'webscrape-cache'),
1393
+ defaultTtl: 1000 * 60 * 60,
1394
+ });
1395
+
1396
+ // Crawler com cache
1397
+ const crawler = new PlaywrightCrawler({
1398
+ maxConcurrency: 3,
1399
+ requestHandlerTimeoutSecs: 30,
1400
+ async requestHandler({ page, request }) {
1401
+ const url = request.url;
1402
+
1403
+ // Verificar cache
1404
+ const cached = await cache.get(url);
1405
+ if (cached) {
1406
+ console.log(`Cache HIT: ${url}`);
1407
+ // Processar HTML do cache
1408
+ return;
1409
+ }
1410
+
1411
+ // Cache miss — buscar
1412
+ console.log(`Cache MISS: ${url}`);
1413
+ const html = await page.content();
1414
+
1415
+ // Armazenar em cache
1416
+ await cache.set(url, html);
1417
+
1418
+ // Processar...
1419
+ },
1420
+ });
1421
+
1422
+ await crawler.run(['https://example.com']);
1423
+
1424
+ // Estatísticas
1425
+ console.log(`Cache size: ${await cache.size()}`);
1426
+ ```
1427
+
1428
+ ### Exemplo 2: Cache com ETag e Revalidação
1429
+
1430
+ ```ts
1431
+ import got from 'got';
1432
+
1433
+ interface CachedResponse {
1434
+ body: string;
1435
+ etag?: string;
1436
+ lastModified?: string;
1437
+ expiry: number;
1438
+ }
1439
+
1440
+ class ETagRevalidatingCache {
1441
+ private cache = new Map<string, CachedResponse>();
1442
+
1443
+ async fetch(url: string, ttl = 300_000): Promise<string> {
1444
+ const cached = this.cache.get(url);
1445
+ const now = Date.now();
1446
+
1447
+ if (cached && cached.expiry > now) {
1448
+ return cached.body;
1449
+ }
1450
+
1451
+ const headers: Record<string, string> = {};
1452
+ if (cached?.etag) headers['If-None-Match'] = cached.etag;
1453
+ if (cached?.lastModified) headers['If-Modified-Since'] = cached.lastModified;
1454
+
1455
+ const response = await got(url, {
1456
+ headers,
1457
+ throwHttpErrors: false,
1458
+ });
1459
+
1460
+ if (response.statusCode === 304 && cached) {
1461
+ this.cache.set(url, { ...cached, expiry: now + ttl });
1462
+ return cached.body;
1463
+ }
1464
+
1465
+ const body = response.body;
1466
+ const newCached: CachedResponse = {
1467
+ body,
1468
+ etag: response.headers['etag'] ?? cached?.etag,
1469
+ lastModified: response.headers['last-modified'] ?? cached?.lastModified,
1470
+ expiry: now + ttl,
1471
+ };
1472
+
1473
+ this.cache.set(url, newCached);
1474
+ return body;
1475
+ }
1476
+ }
1477
+ ```
1478
+
1479
+ ### Exemplo 3: Cache com Playwright e Intercepção
1480
+
1481
+ ```ts
1482
+ import { chromium } from 'playwright';
1483
+
1484
+ class PlaywrightMultiLayerCache {
1485
+ private memoryCache = new Map<string, { body: Buffer; contentType: string }>();
1486
+
1487
+ async scrapeWithCache(url: string): Promise<{ html: string; fromCache: boolean }> {
1488
+ const browser = await chromium.launch();
1489
+ const page = await browser.newPage();
1490
+
1491
+ let fromCache = false;
1492
+
1493
+ await page.route('**/*', async (route) => {
1494
+ const requestUrl = route.request().url();
1495
+ const resourceType = route.request().resourceType();
1496
+
1497
+ // Cache apenas para páginas HTML
1498
+ if (resourceType !== 'document') {
1499
+ await route.continue();
1500
+ return;
1501
+ }
1502
+
1503
+ const cached = this.memoryCache.get(requestUrl);
1504
+ if (cached) {
1505
+ fromCache = true;
1506
+ await route.fulfill({
1507
+ status: 200,
1508
+ contentType: cached.contentType,
1509
+ body: cached.body,
1510
+ });
1511
+ return;
1512
+ }
1513
+
1514
+ const response = await route.fetch();
1515
+ const body = await response.body();
1516
+
1517
+ if (response.status() === 200) {
1518
+ this.memoryCache.set(requestUrl, {
1519
+ body,
1520
+ contentType: response.headers()['content-type'] ?? 'text/html',
1521
+ });
1522
+ }
1523
+
1524
+ await route.fulfill({
1525
+ status: response.status(),
1526
+ headers: response.headers(),
1527
+ body,
1528
+ });
1529
+ });
1530
+
1531
+ await page.goto(url, { waitUntil: 'networkidle' });
1532
+ const html = await page.content();
1533
+ await browser.close();
1534
+
1535
+ return { html, fromCache };
1536
+ }
1537
+ }
1538
+
1539
+ // Usar
1540
+ const cache = new PlaywrightMultiLayerCache();
1541
+ const result = await cache.scrapeWithCache('https://example.com');
1542
+ console.log(`From cache: ${result.fromCache}`);
1543
+ ```
1544
+
1545
+ ### Exemplo 4: Cache Manager com Múltiplos Backends
1546
+
1547
+ ```ts
1548
+ import { caching } from 'cache-manager';
1549
+
1550
+ async function createMultiBackendCache() {
1551
+ // L1: Memória
1552
+ const l1 = await caching('memory', {
1553
+ max: 500,
1554
+ ttl: 300, // 5 min
1555
+ });
1556
+
1557
+ // L2: Redis (se disponível)
1558
+ // const l2 = await caching(redisStore, {
1559
+ // host: 'localhost',
1560
+ // port: 6379,
1561
+ // ttl: 3600, // 1 hora
1562
+ // });
1563
+
1564
+ async function get<T>(key: string): Promise<T | null> {
1565
+ // L1
1566
+ const l1Value = await l1.get<T>(key);
1567
+ if (l1Value !== undefined) return l1Value;
1568
+
1569
+ // L2 (se disponível)
1570
+ // const l2Value = await l2.get<T>(key);
1571
+ // if (l2Value !== undefined) {
1572
+ // await l1.set(key, l2Value, 300); // Promover para L1
1573
+ // return l2Value;
1574
+ // }
1575
+
1576
+ return null;
1577
+ }
1578
+
1579
+ async function set<T>(key: string, value: T, ttl?: number): Promise<void> {
1580
+ await l1.set(key, value, ttl ?? 300);
1581
+ // await l2.set(key, value, ttl ?? 3600);
1582
+ }
1583
+
1584
+ async function invalidate(key: string): Promise<void> {
1585
+ await l1.del(key);
1586
+ // await l2.del(key);
1587
+ }
1588
+
1589
+ return { get, set, invalidate };
1590
+ }
1591
+
1592
+ const cache = await createMultiBackendCache();
1593
+ await cache.set('key', 'value');
1594
+ const value = await cache.get<string>('key');
1595
+ ```
1596
+
1597
+ ---
1598
+
1599
+ ## Referências
1600
+
1601
+ - [lru-cache npm](https://www.npmjs.com/package/lru-cache)
1602
+ - [node-cache npm](https://www.npmjs.com/package/node-cache)
1603
+ - [quick-lru npm](https://www.npmjs.com/package/quick-lru)
1604
+ - [cache-manager npm](https://www.npmjs.com/package/cache-manager)
1605
+ - [keyv npm](https://www.npmjs.com/package/keyv)
1606
+ - [cacache npm](https://www.npmjs.com/package/cacache)
1607
+ - [flat-cache npm](https://www.npmjs.com/package/flat-cache)
1608
+ - [got-scraping npm](https://www.npmjs.com/package/got-scraping)
1609
+ - [Playwright Network Interception](https://playwright.dev/docs/network)
1610
+ - [Crawlee Documentation](https://crawlee.dev)