@nitpicker/crawler 0.11.0 → 0.13.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.
Files changed (387) hide show
  1. package/README.md +6 -4
  2. package/lib/archive/archive-accessor.d.ts +2 -2
  3. package/lib/archive/archive-accessor.js +2 -2
  4. package/lib/archive/archive-lock.d.ts +7 -0
  5. package/lib/archive/archive-lock.js +7 -0
  6. package/lib/archive/archive.d.ts +175 -13
  7. package/lib/archive/archive.js +198 -14
  8. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  9. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  10. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  11. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  12. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  13. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  14. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  15. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  16. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  17. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  18. package/lib/archive/create-adjunct-tables.d.ts +43 -0
  19. package/lib/archive/create-adjunct-tables.js +213 -0
  20. package/lib/archive/create-entity-tables.d.ts +173 -0
  21. package/lib/archive/create-entity-tables.js +318 -0
  22. package/lib/archive/create-progress-reporter.d.ts +30 -0
  23. package/lib/archive/create-progress-reporter.js +38 -0
  24. package/lib/archive/create-ref-tables.d.ts +35 -0
  25. package/lib/archive/create-ref-tables.js +188 -0
  26. package/lib/archive/database.d.ts +130 -182
  27. package/lib/archive/database.js +524 -1777
  28. package/lib/archive/db-ops/_shared/clear-write-ref-caches.d.ts +27 -0
  29. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +34 -0
  30. package/lib/archive/db-ops/_shared/create-write-ref-caches.d.ts +17 -0
  31. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +26 -0
  32. package/lib/archive/db-ops/_shared/decode-json-ref.d.ts +17 -0
  33. package/lib/archive/db-ops/_shared/decode-json-ref.js +31 -0
  34. package/lib/archive/db-ops/_shared/load-response-headers-by-set-ids.d.ts +20 -0
  35. package/lib/archive/db-ops/_shared/load-response-headers-by-set-ids.js +53 -0
  36. package/lib/archive/db-ops/_shared/resolve-content-item-id.d.ts +61 -0
  37. package/lib/archive/db-ops/_shared/resolve-content-item-id.js +111 -0
  38. package/lib/archive/db-ops/_shared/resolve-url-or-blob.d.ts +23 -0
  39. package/lib/archive/db-ops/_shared/resolve-url-or-blob.js +29 -0
  40. package/lib/archive/db-ops/_shared/retry-setting.d.ts +16 -0
  41. package/lib/archive/db-ops/_shared/retry-setting.js +18 -0
  42. package/lib/archive/db-ops/_shared/safe-parse-json.d.ts +11 -0
  43. package/lib/archive/db-ops/_shared/safe-parse-json.js +18 -0
  44. package/lib/archive/db-ops/_shared/types.d.ts +53 -0
  45. package/lib/archive/db-ops/_shared/types.js +1 -0
  46. package/lib/archive/db-ops/_shared/upsert-blob-ref.d.ts +25 -0
  47. package/lib/archive/db-ops/_shared/upsert-blob-ref.js +48 -0
  48. package/lib/archive/db-ops/_shared/upsert-content-type-ref.d.ts +30 -0
  49. package/lib/archive/db-ops/_shared/upsert-content-type-ref.js +45 -0
  50. package/lib/archive/db-ops/_shared/upsert-json-ref.d.ts +22 -0
  51. package/lib/archive/db-ops/_shared/upsert-json-ref.js +41 -0
  52. package/lib/archive/db-ops/_shared/upsert-response-headers.d.ts +35 -0
  53. package/lib/archive/db-ops/_shared/upsert-response-headers.js +49 -0
  54. package/lib/archive/db-ops/_shared/upsert-url-ref.d.ts +39 -0
  55. package/lib/archive/db-ops/_shared/upsert-url-ref.js +62 -0
  56. package/lib/archive/db-ops/analysis/replace-analysis-violations.d.ts +28 -0
  57. package/lib/archive/db-ops/analysis/replace-analysis-violations.js +152 -0
  58. package/lib/archive/db-ops/anchors/get-anchors-on-page.d.ts +10 -0
  59. package/lib/archive/db-ops/anchors/get-anchors-on-page.js +21 -0
  60. package/lib/archive/db-ops/config/get-base-url.d.ts +8 -0
  61. package/lib/archive/db-ops/config/get-base-url.js +14 -0
  62. package/lib/archive/db-ops/config/get-config.d.ts +10 -0
  63. package/lib/archive/db-ops/config/get-config.js +27 -0
  64. package/lib/archive/db-ops/config/get-name.d.ts +8 -0
  65. package/lib/archive/db-ops/config/get-name.js +14 -0
  66. package/lib/archive/db-ops/config/info-column-allowlist.d.ts +7 -0
  67. package/lib/archive/db-ops/config/info-column-allowlist.js +26 -0
  68. package/lib/archive/db-ops/config/info-json-columns.d.ts +5 -0
  69. package/lib/archive/db-ops/config/info-json-columns.js +10 -0
  70. package/lib/archive/db-ops/config/set-config.d.ts +12 -0
  71. package/lib/archive/db-ops/config/set-config.js +21 -0
  72. package/lib/archive/db-ops/config/update-config.d.ts +17 -0
  73. package/lib/archive/db-ops/config/update-config.js +36 -0
  74. package/lib/archive/db-ops/errors/insert-crawl-error.d.ts +15 -0
  75. package/lib/archive/db-ops/errors/insert-crawl-error.js +21 -0
  76. package/lib/archive/db-ops/errors/insert-page-error.d.ts +21 -0
  77. package/lib/archive/db-ops/errors/insert-page-error.js +28 -0
  78. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +22 -0
  79. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +141 -0
  80. package/lib/archive/db-ops/html/get-html-of-page-by-id.d.ts +18 -0
  81. package/lib/archive/db-ops/html/get-html-of-page-by-id.js +29 -0
  82. package/lib/archive/db-ops/inventory/record-inventory-run.d.ts +21 -0
  83. package/lib/archive/db-ops/inventory/record-inventory-run.js +38 -0
  84. package/lib/archive/db-ops/lifecycle/checkpoint.d.ts +8 -0
  85. package/lib/archive/db-ops/lifecycle/checkpoint.js +9 -0
  86. package/lib/archive/db-ops/lifecycle/destroy.d.ts +6 -0
  87. package/lib/archive/db-ops/lifecycle/destroy.js +7 -0
  88. package/lib/archive/db-ops/lifecycle/init.d.ts +22 -0
  89. package/lib/archive/db-ops/lifecycle/init.js +42 -0
  90. package/lib/archive/db-ops/meta/get-jsonld-of-page.d.ts +13 -0
  91. package/lib/archive/db-ops/meta/get-jsonld-of-page.js +27 -0
  92. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +12 -0
  93. package/lib/archive/db-ops/meta/get-tags-of-page.js +28 -0
  94. package/lib/archive/db-ops/pages/order/set-url-order.d.ts +8 -0
  95. package/lib/archive/db-ops/pages/order/set-url-order.js +32 -0
  96. package/lib/archive/db-ops/pages/read/build-page-query.d.ts +18 -0
  97. package/lib/archive/db-ops/pages/read/build-page-query.js +40 -0
  98. package/lib/archive/db-ops/pages/read/get-crawling-state.d.ts +70 -0
  99. package/lib/archive/db-ops/pages/read/get-crawling-state.js +98 -0
  100. package/lib/archive/db-ops/pages/read/get-existing-page-urls.d.ts +15 -0
  101. package/lib/archive/db-ops/pages/read/get-existing-page-urls.js +30 -0
  102. package/lib/archive/db-ops/pages/read/get-page-count.d.ts +12 -0
  103. package/lib/archive/db-ops/pages/read/get-page-count.js +21 -0
  104. package/lib/archive/db-ops/pages/read/get-page-source-by-url.d.ts +24 -0
  105. package/lib/archive/db-ops/pages/read/get-page-source-by-url.js +28 -0
  106. package/lib/archive/db-ops/pages/read/get-pages-with-rels.d.ts +38 -0
  107. package/lib/archive/db-ops/pages/read/get-pages-with-rels.js +107 -0
  108. package/lib/archive/db-ops/pages/read/get-pages.d.ts +11 -0
  109. package/lib/archive/db-ops/pages/read/get-pages.js +51 -0
  110. package/lib/archive/db-ops/pages/read/get-scraped-html-page-count.d.ts +18 -0
  111. package/lib/archive/db-ops/pages/read/get-scraped-html-page-count.js +25 -0
  112. package/lib/archive/db-ops/pages/read/reconstruct-page-rows.d.ts +31 -0
  113. package/lib/archive/db-ops/pages/read/reconstruct-page-rows.js +32 -0
  114. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +24 -0
  115. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +93 -0
  116. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +47 -0
  117. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +124 -0
  118. package/lib/archive/db-ops/pages/write/insert-inventory-seeds.d.ts +37 -0
  119. package/lib/archive/db-ops/pages/write/insert-inventory-seeds.js +72 -0
  120. package/lib/archive/db-ops/pages/write/insert-jsonld.d.ts +17 -0
  121. package/lib/archive/db-ops/pages/write/insert-jsonld.js +49 -0
  122. package/lib/archive/db-ops/pages/write/insert-page.d.ts +36 -0
  123. package/lib/archive/db-ops/pages/write/insert-page.js +208 -0
  124. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +16 -0
  125. package/lib/archive/db-ops/pages/write/insert-tags.js +34 -0
  126. package/lib/archive/db-ops/pages/write/link-redirect-sources.d.ts +36 -0
  127. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +93 -0
  128. package/lib/archive/db-ops/pages/write/record-redirect.d.ts +35 -0
  129. package/lib/archive/db-ops/pages/write/record-redirect.js +100 -0
  130. package/lib/archive/db-ops/pages/write/set-skipped-page.d.ts +13 -0
  131. package/lib/archive/db-ops/pages/write/set-skipped-page.js +22 -0
  132. package/lib/archive/db-ops/pages/write/update-page.d.ts +29 -0
  133. package/lib/archive/db-ops/pages/write/update-page.js +334 -0
  134. package/lib/archive/db-ops/pages/write/write-page-html-blob.d.ts +19 -0
  135. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +41 -0
  136. package/lib/archive/db-ops/referrers/get-redirects-for-pages.d.ts +9 -0
  137. package/lib/archive/db-ops/referrers/get-redirects-for-pages.js +15 -0
  138. package/lib/archive/db-ops/referrers/get-referrers-of-page.d.ts +17 -0
  139. package/lib/archive/db-ops/referrers/get-referrers-of-page.js +32 -0
  140. package/lib/archive/db-ops/referrers/get-referrers-of-resource.d.ts +8 -0
  141. package/lib/archive/db-ops/referrers/get-referrers-of-resource.js +15 -0
  142. package/lib/archive/db-ops/resources/build-resource-query.d.ts +25 -0
  143. package/lib/archive/db-ops/resources/build-resource-query.js +29 -0
  144. package/lib/archive/db-ops/resources/get-existing-resource-urls.d.ts +9 -0
  145. package/lib/archive/db-ops/resources/get-existing-resource-urls.js +24 -0
  146. package/lib/archive/db-ops/resources/get-resource-by-url.d.ts +13 -0
  147. package/lib/archive/db-ops/resources/get-resource-by-url.js +22 -0
  148. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +9 -0
  149. package/lib/archive/db-ops/resources/get-resource-url-list.js +13 -0
  150. package/lib/archive/db-ops/resources/get-resources.d.ts +8 -0
  151. package/lib/archive/db-ops/resources/get-resources.js +11 -0
  152. package/lib/archive/db-ops/resources/insert-inventory-resources.d.ts +24 -0
  153. package/lib/archive/db-ops/resources/insert-inventory-resources.js +64 -0
  154. package/lib/archive/db-ops/resources/insert-resource-referrers.d.ts +15 -0
  155. package/lib/archive/db-ops/resources/insert-resource-referrers.js +54 -0
  156. package/lib/archive/db-ops/resources/insert-resource.d.ts +34 -0
  157. package/lib/archive/db-ops/resources/insert-resource.js +73 -0
  158. package/lib/archive/db-ops/resources/reconstruct-resource-rows.d.ts +26 -0
  159. package/lib/archive/db-ops/resources/reconstruct-resource-rows.js +30 -0
  160. package/lib/archive/decode-html-blob.d.ts +18 -0
  161. package/lib/archive/decode-html-blob.js +31 -0
  162. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  163. package/lib/archive/derive-lineage-from-parent.js +42 -0
  164. package/lib/archive/drop-legacy-tables.d.ts +45 -0
  165. package/lib/archive/drop-legacy-tables.js +56 -0
  166. package/lib/archive/filesystem/rename.js +1 -1
  167. package/lib/archive/get-failed-page-messages.d.ts +44 -0
  168. package/lib/archive/get-failed-page-messages.js +132 -0
  169. package/lib/archive/init-schema.d.ts +35 -39
  170. package/lib/archive/init-schema.js +111 -320
  171. package/lib/archive/is-inventory-source.d.ts +21 -0
  172. package/lib/archive/is-inventory-source.js +22 -0
  173. package/lib/archive/limited-page-ids.d.ts +2 -1
  174. package/lib/archive/limited-page-ids.js +5 -4
  175. package/lib/archive/meta/assert-compatible-version.d.ts +24 -3
  176. package/lib/archive/meta/assert-compatible-version.js +24 -3
  177. package/lib/archive/meta/types.d.ts +87 -1
  178. package/lib/archive/meta/types.js +34 -2
  179. package/lib/archive/migrate-entity-tables.d.ts +45 -0
  180. package/lib/archive/migrate-entity-tables.js +56 -0
  181. package/lib/archive/migrate-ref-tables.d.ts +25 -0
  182. package/lib/archive/migrate-ref-tables.js +38 -0
  183. package/lib/archive/page-meta-column-maps.d.ts +32 -0
  184. package/lib/archive/page-meta-column-maps.js +43 -0
  185. package/lib/archive/page.d.ts +6 -6
  186. package/lib/archive/page.js +5 -5
  187. package/lib/archive/peek-archive-lock.d.ts +2 -2
  188. package/lib/archive/peek-archive-lock.js +2 -2
  189. package/lib/archive/populate-entity-tables/collapse-anchor-rows.d.ts +41 -0
  190. package/lib/archive/populate-entity-tables/collapse-anchor-rows.js +87 -0
  191. package/lib/archive/populate-entity-tables/derive-dom-path.d.ts +35 -0
  192. package/lib/archive/populate-entity-tables/derive-dom-path.js +72 -0
  193. package/lib/archive/populate-entity-tables/is-blob-ref-value.d.ts +16 -0
  194. package/lib/archive/populate-entity-tables/is-blob-ref-value.js +19 -0
  195. package/lib/archive/populate-entity-tables/match-images-to-dom-paths.d.ts +66 -0
  196. package/lib/archive/populate-entity-tables/match-images-to-dom-paths.js +96 -0
  197. package/lib/archive/populate-entity-tables/populate-anchor-edges.d.ts +33 -0
  198. package/lib/archive/populate-entity-tables/populate-anchor-edges.js +153 -0
  199. package/lib/archive/populate-entity-tables/populate-content-items.d.ts +40 -0
  200. package/lib/archive/populate-entity-tables/populate-content-items.js +141 -0
  201. package/lib/archive/populate-entity-tables/populate-entities.d.ts +81 -0
  202. package/lib/archive/populate-entity-tables/populate-entities.js +111 -0
  203. package/lib/archive/populate-entity-tables/populate-image-items.d.ts +91 -0
  204. package/lib/archive/populate-entity-tables/populate-image-items.js +223 -0
  205. package/lib/archive/populate-entity-tables/populate-page-meta.d.ts +33 -0
  206. package/lib/archive/populate-entity-tables/populate-page-meta.js +267 -0
  207. package/lib/archive/populate-entity-tables/populate-resource-items.d.ts +22 -0
  208. package/lib/archive/populate-entity-tables/populate-resource-items.js +114 -0
  209. package/lib/archive/populate-entity-tables/populate-resource-ref-edges.d.ts +31 -0
  210. package/lib/archive/populate-entity-tables/populate-resource-ref-edges.js +33 -0
  211. package/lib/archive/populate-entity-tables/resolve-blob-refs.d.ts +31 -0
  212. package/lib/archive/populate-entity-tables/resolve-blob-refs.js +100 -0
  213. package/lib/archive/populate-entity-tables/resolve-content-type-refs.d.ts +22 -0
  214. package/lib/archive/populate-entity-tables/resolve-content-type-refs.js +27 -0
  215. package/lib/archive/populate-entity-tables/resolve-header-sets.d.ts +49 -0
  216. package/lib/archive/populate-entity-tables/resolve-header-sets.js +122 -0
  217. package/lib/archive/populate-entity-tables/resolve-json-refs.d.ts +25 -0
  218. package/lib/archive/populate-entity-tables/resolve-json-refs.js +67 -0
  219. package/lib/archive/populate-entity-tables/resolve-text-refs.d.ts +30 -0
  220. package/lib/archive/populate-entity-tables/resolve-text-refs.js +61 -0
  221. package/lib/archive/populate-entity-tables/resolve-url-or-blob-from-maps.d.ts +21 -0
  222. package/lib/archive/populate-entity-tables/resolve-url-or-blob-from-maps.js +27 -0
  223. package/lib/archive/populate-entity-tables/resolve-url-refs.d.ts +33 -0
  224. package/lib/archive/populate-entity-tables/resolve-url-refs.js +60 -0
  225. package/lib/archive/populate-entity-tables/test-utils/count-rows.d.ts +17 -0
  226. package/lib/archive/populate-entity-tables/test-utils/count-rows.js +20 -0
  227. package/lib/archive/populate-entity-tables/test-utils/seed-content-items.d.ts +25 -0
  228. package/lib/archive/populate-entity-tables/test-utils/seed-content-items.js +42 -0
  229. package/lib/archive/populate-entity-tables/test-utils/setup-entities-db.d.ts +23 -0
  230. package/lib/archive/populate-entity-tables/test-utils/setup-entities-db.js +178 -0
  231. package/lib/archive/populate-entity-tables/types.d.ts +157 -0
  232. package/lib/archive/populate-entity-tables/types.js +12 -0
  233. package/lib/archive/populate-entity-tables/upsert-text-refs.d.ts +38 -0
  234. package/lib/archive/populate-entity-tables/upsert-text-refs.js +78 -0
  235. package/lib/archive/populate-ref-tables/classify-content-type.d.ts +16 -0
  236. package/lib/archive/populate-ref-tables/classify-content-type.js +52 -0
  237. package/lib/archive/populate-ref-tables/compute-content-hash.d.ts +22 -0
  238. package/lib/archive/populate-ref-tables/compute-content-hash.js +26 -0
  239. package/lib/archive/populate-ref-tables/compute-header-flags.d.ts +16 -0
  240. package/lib/archive/populate-ref-tables/compute-header-flags.js +70 -0
  241. package/lib/archive/populate-ref-tables/content-type-rules.d.ts +38 -0
  242. package/lib/archive/populate-ref-tables/content-type-rules.js +133 -0
  243. package/lib/archive/populate-ref-tables/create-header-table-caches.d.ts +25 -0
  244. package/lib/archive/populate-ref-tables/create-header-table-caches.js +49 -0
  245. package/lib/archive/populate-ref-tables/data-uri-url-refs-limit.d.ts +15 -0
  246. package/lib/archive/populate-ref-tables/data-uri-url-refs-limit.js +15 -0
  247. package/lib/archive/populate-ref-tables/decode-data-uri.d.ts +21 -0
  248. package/lib/archive/populate-ref-tables/decode-data-uri.js +126 -0
  249. package/lib/archive/populate-ref-tables/decompose-header-set.d.ts +29 -0
  250. package/lib/archive/populate-ref-tables/decompose-header-set.js +157 -0
  251. package/lib/archive/populate-ref-tables/decompose-url.d.ts +25 -0
  252. package/lib/archive/populate-ref-tables/decompose-url.js +70 -0
  253. package/lib/archive/populate-ref-tables/header-stability.d.ts +19 -0
  254. package/lib/archive/populate-ref-tables/header-stability.js +22 -0
  255. package/lib/archive/populate-ref-tables/header-value-cache-key.d.ts +17 -0
  256. package/lib/archive/populate-ref-tables/header-value-cache-key.js +19 -0
  257. package/lib/archive/populate-ref-tables/normalize-mime.d.ts +24 -0
  258. package/lib/archive/populate-ref-tables/normalize-mime.js +36 -0
  259. package/lib/archive/populate-ref-tables/populate-blob-refs.d.ts +38 -0
  260. package/lib/archive/populate-ref-tables/populate-blob-refs.js +134 -0
  261. package/lib/archive/populate-ref-tables/populate-content-type-refs.d.ts +27 -0
  262. package/lib/archive/populate-ref-tables/populate-content-type-refs.js +70 -0
  263. package/lib/archive/populate-ref-tables/populate-header-tables.d.ts +35 -0
  264. package/lib/archive/populate-ref-tables/populate-header-tables.js +80 -0
  265. package/lib/archive/populate-ref-tables/populate-json-refs.d.ts +29 -0
  266. package/lib/archive/populate-ref-tables/populate-json-refs.js +101 -0
  267. package/lib/archive/populate-ref-tables/populate-refs.d.ts +51 -0
  268. package/lib/archive/populate-ref-tables/populate-refs.js +62 -0
  269. package/lib/archive/populate-ref-tables/populate-text-refs.d.ts +32 -0
  270. package/lib/archive/populate-ref-tables/populate-text-refs.js +133 -0
  271. package/lib/archive/populate-ref-tables/populate-url-refs.d.ts +28 -0
  272. package/lib/archive/populate-ref-tables/populate-url-refs.js +148 -0
  273. package/lib/archive/populate-ref-tables/test-utils/count-rows.d.ts +15 -0
  274. package/lib/archive/populate-ref-tables/test-utils/count-rows.js +17 -0
  275. package/lib/archive/populate-ref-tables/types.d.ts +197 -0
  276. package/lib/archive/populate-ref-tables/types.js +7 -0
  277. package/lib/archive/populate-ref-tables/upsert-one-header-set.d.ts +34 -0
  278. package/lib/archive/populate-ref-tables/upsert-one-header-set.js +208 -0
  279. package/lib/archive/populate-ref-tables/volatile-header-names.d.ts +20 -0
  280. package/lib/archive/populate-ref-tables/volatile-header-names.js +33 -0
  281. package/lib/archive/redirect-table.d.ts +4 -2
  282. package/lib/archive/redirect-table.js +15 -10
  283. package/lib/archive/resolve-redirect-chain.d.ts +3 -3
  284. package/lib/archive/resolve-redirect-chain.js +2 -2
  285. package/lib/archive/resource.d.ts +1 -1
  286. package/lib/archive/retarget-legacy-fk-tables.d.ts +47 -0
  287. package/lib/archive/retarget-legacy-fk-tables.js +107 -0
  288. package/lib/archive/test-utils/fk-parent-tables.d.ts +15 -0
  289. package/lib/archive/test-utils/fk-parent-tables.js +19 -0
  290. package/lib/archive/test-utils/seed-content-item.d.ts +35 -0
  291. package/lib/archive/test-utils/seed-content-item.js +42 -0
  292. package/lib/archive/test-utils/setup-legacy-fk-db.d.ts +33 -0
  293. package/lib/archive/test-utils/setup-legacy-fk-db.js +270 -0
  294. package/lib/archive/types.d.ts +151 -15
  295. package/lib/archive/verify-migration/capture-rejection.d.ts +24 -0
  296. package/lib/archive/verify-migration/capture-rejection.js +31 -0
  297. package/lib/archive/verify-migration/check-anchor-edges-count.d.ts +34 -0
  298. package/lib/archive/verify-migration/check-anchor-edges-count.js +72 -0
  299. package/lib/archive/verify-migration/check-anchor-edges-sum.d.ts +13 -0
  300. package/lib/archive/verify-migration/check-anchor-edges-sum.js +27 -0
  301. package/lib/archive/verify-migration/check-content-items-count.d.ts +16 -0
  302. package/lib/archive/verify-migration/check-content-items-count.js +30 -0
  303. package/lib/archive/verify-migration/check-content-type-preservation.d.ts +22 -0
  304. package/lib/archive/verify-migration/check-content-type-preservation.js +40 -0
  305. package/lib/archive/verify-migration/check-foreign-key-integrity.d.ts +31 -0
  306. package/lib/archive/verify-migration/check-foreign-key-integrity.js +47 -0
  307. package/lib/archive/verify-migration/check-image-items-count.d.ts +12 -0
  308. package/lib/archive/verify-migration/check-image-items-count.js +26 -0
  309. package/lib/archive/verify-migration/check-page-meta-count.d.ts +15 -0
  310. package/lib/archive/verify-migration/check-page-meta-count.js +31 -0
  311. package/lib/archive/verify-migration/check-reader-parity.d.ts +23 -0
  312. package/lib/archive/verify-migration/check-reader-parity.js +211 -0
  313. package/lib/archive/verify-migration/check-resource-items-count.d.ts +17 -0
  314. package/lib/archive/verify-migration/check-resource-items-count.js +33 -0
  315. package/lib/archive/verify-migration/check-url-round-trip.d.ts +43 -0
  316. package/lib/archive/verify-migration/check-url-round-trip.js +112 -0
  317. package/lib/archive/verify-migration/types.d.ts +70 -0
  318. package/lib/archive/verify-migration/types.js +63 -0
  319. package/lib/archive/verify-migration/verify-migration.d.ts +41 -0
  320. package/lib/archive/verify-migration/verify-migration.js +120 -0
  321. package/lib/classify-error-kind.d.ts +19 -0
  322. package/lib/classify-error-kind.js +122 -0
  323. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  324. package/lib/crawler/build-js-redirect-edge.js +57 -0
  325. package/lib/crawler/build-redirect-event.d.ts +24 -0
  326. package/lib/crawler/build-redirect-event.js +28 -0
  327. package/lib/crawler/capture-image-dom-paths.d.ts +33 -0
  328. package/lib/crawler/capture-image-dom-paths.js +39 -0
  329. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  330. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  331. package/lib/crawler/collect-image-dom-paths.d.ts +23 -0
  332. package/lib/crawler/collect-image-dom-paths.js +64 -0
  333. package/lib/crawler/crawler.d.ts +22 -1
  334. package/lib/crawler/crawler.js +679 -117
  335. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  336. package/lib/crawler/derive-js-redirect-target.js +129 -0
  337. package/lib/crawler/derive-resource-source.d.ts +25 -15
  338. package/lib/crawler/derive-resource-source.js +28 -17
  339. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  340. package/lib/crawler/dns-burned-host-cache.js +25 -0
  341. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  342. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  343. package/lib/crawler/fetch-destination.d.ts +12 -4
  344. package/lib/crawler/fetch-destination.js +94 -16
  345. package/lib/crawler/inject-scope-auth.d.ts +1 -1
  346. package/lib/crawler/inject-scope-auth.js +1 -1
  347. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  348. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  349. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  350. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  351. package/lib/crawler/link-list.d.ts +21 -1
  352. package/lib/crawler/link-list.js +23 -3
  353. package/lib/crawler/normalize-content-type.d.ts +1 -1
  354. package/lib/crawler/normalize-content-type.js +1 -1
  355. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  356. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  357. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  358. package/lib/crawler/preload-short-circuit-error.js +25 -0
  359. package/lib/crawler/should-burn-host.d.ts +78 -0
  360. package/lib/crawler/should-burn-host.js +61 -0
  361. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  362. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  363. package/lib/crawler/types.d.ts +108 -1
  364. package/lib/crawler-orchestrator.d.ts +22 -3
  365. package/lib/crawler-orchestrator.js +312 -73
  366. package/lib/crawler.d.ts +15 -2
  367. package/lib/crawler.js +24 -1
  368. package/lib/permanent-error-kinds.d.ts +43 -0
  369. package/lib/permanent-error-kinds.js +48 -0
  370. package/lib/types.d.ts +84 -0
  371. package/lib/utils/compute-file-sha256.d.ts +24 -0
  372. package/lib/utils/compute-file-sha256.js +56 -0
  373. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  374. package/lib/utils/error/emit-error-with-retry.js +44 -0
  375. package/lib/utils/error/emit-error.d.ts +39 -0
  376. package/lib/utils/error/emit-error.js +41 -0
  377. package/package.json +12 -12
  378. package/lib/archive/migrate-crawl-errors.d.ts +0 -20
  379. package/lib/archive/migrate-crawl-errors.js +0 -38
  380. package/lib/archive/migrate-html-blob-tables.d.ts +0 -24
  381. package/lib/archive/migrate-html-blob-tables.js +0 -53
  382. package/lib/archive/migrate-page-errors.d.ts +0 -16
  383. package/lib/archive/migrate-page-errors.js +0 -35
  384. package/lib/archive/migrate-pages-resources-source.d.ts +0 -16
  385. package/lib/archive/migrate-pages-resources-source.js +0 -46
  386. package/lib/utils/error/error-emitter.d.ts +0 -18
  387. package/lib/utils/error/error-emitter.js +0 -29
@@ -0,0 +1,95 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ /**
5
+ * Number of bytes hashed from each end of the file to corner the case
6
+ * where two different archives share `size + mtime + ctime` by accident.
7
+ *
8
+ * 64 KiB is a sweet spot: on a 10 GB archive it reads 0.0006 % of the
9
+ * file (~1 ms on an SSD, ~5 ms on a spinning disk) while reliably
10
+ * sampling enough of both the tar leading header AND the tail (which
11
+ * for tar archives contains the last-written entry's data, so any real
12
+ * mutation moves at least one of the two windows).
13
+ */
14
+ const HEAD_TAIL_HASH_BYTES = 64 * 1024;
15
+ /**
16
+ * Cache-key segment substituted when a file is smaller than the
17
+ * head+tail sample window. In that case the head segment alone already
18
+ * spans the whole file (size column also rolls), so a separate tail
19
+ * digest is meaningless and we encode that explicitly rather than
20
+ * silently overlapping reads.
21
+ */
22
+ const TAIL_NOT_NEEDED = 'short';
23
+ /**
24
+ * Derive a stable cache key from a `.nitpicker` file's inode metadata
25
+ * plus a head+tail content sample.
26
+ *
27
+ * Metadata fields:
28
+ *
29
+ * - `size` covers the bulk of accidental cache collision risk in O(1).
30
+ * - `mtime` (mod time) changes whenever the file's content is rewritten,
31
+ * which is the common case (`crawl --append`, `crawl --retry-failed`,
32
+ * `cp -f`, rsync).
33
+ * - `ctime` (inode change time) closes the `touch -m -t <past>` loophole:
34
+ * even if a user resets `mtime` to fake "unchanged", the act of touching
35
+ * bumps `ctime` on POSIX so the key still rolls.
36
+ *
37
+ * Head + tail digest:
38
+ *
39
+ * - On filesystems with low-resolution timestamps (FAT / exFAT / NFSv3 /
40
+ * some Docker volume mounts where mtime/ctime are second-granular and
41
+ * sometimes don't move on small appends), `size + mtime + ctime`
42
+ * alone can stay identical across an in-place rewrite.
43
+ * - We hash the first {@link HEAD_TAIL_HASH_BYTES} bytes and the last
44
+ * {@link HEAD_TAIL_HASH_BYTES} bytes to detect this. For tar archives
45
+ * the head holds the first entry's header (which moves when the inner
46
+ * directory's name changes) and the tail holds the last entry's data
47
+ * (which always moves on `crawl --append` because the appended pages
48
+ * land near the end of the tar stream).
49
+ * - Full-content sha256 was rejected: ~20-30 s on a 10 GB archive,
50
+ * which is slower than the untar this cache is meant to avoid.
51
+ *
52
+ * The key is stable across symlinks (the caller resolves via
53
+ * `fs.realpath` upstream) but **not** across hardlinks pointing at a
54
+ * mutated inode — that is intentional, hardlinking is a power-user
55
+ * move and the user is expected to know the cache will share an entry.
56
+ * @param archivePath - Absolute path to the `.nitpicker` file.
57
+ * @returns A string of the form
58
+ * `<size>-<mtime_ns>-<ctime_ns>-<headHex>-<tailHex>` suitable for use
59
+ * as a directory-name component.
60
+ */
61
+ export async function computeArchiveCacheKey(archivePath) {
62
+ const stats = await fs.stat(archivePath, { bigint: true });
63
+ const fileSize = Number(stats.size);
64
+ const headHex = await sha256OfRange(archivePath, 0, Math.min(HEAD_TAIL_HASH_BYTES, fileSize));
65
+ const tailHex = fileSize > HEAD_TAIL_HASH_BYTES
66
+ ? await sha256OfRange(archivePath, fileSize - HEAD_TAIL_HASH_BYTES, HEAD_TAIL_HASH_BYTES)
67
+ : TAIL_NOT_NEEDED;
68
+ return `${stats.size}-${stats.mtimeNs}-${stats.ctimeNs}-${headHex}-${tailHex}`;
69
+ }
70
+ /**
71
+ * SHA-256 the requested byte range of a file via a single read stream.
72
+ * Truncated to 16 hex chars (64 bits) because the digest only needs to
73
+ * disambiguate within a `(size, mtime, ctime)` bucket — full 256-bit
74
+ * fingerprints would only waste filesystem path budget.
75
+ * @param filePath - Absolute path to the file.
76
+ * @param start - Inclusive byte offset to begin reading at.
77
+ * @param length - Number of bytes to read; 0 returns the empty-input digest.
78
+ * @returns A 16-character hex string.
79
+ */
80
+ async function sha256OfRange(filePath, start, length) {
81
+ const hash = createHash('sha256');
82
+ if (length === 0) {
83
+ return hash.digest('hex').slice(0, 16);
84
+ }
85
+ await new Promise((resolve, reject) => {
86
+ const stream = createReadStream(filePath, {
87
+ start,
88
+ end: start + length - 1,
89
+ });
90
+ stream.on('data', (chunk) => hash.update(chunk));
91
+ stream.on('end', () => resolve());
92
+ stream.on('error', (error) => reject(error));
93
+ });
94
+ return hash.digest('hex').slice(0, 16);
95
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Untar a `.nitpicker` archive into the given cache directory.
3
+ *
4
+ * Concurrency / re-entry contract:
5
+ *
6
+ * - Two viewers opening the same archive race for the cache lock; the
7
+ * loser waits and recheck-loops until the winner writes the ready
8
+ * marker, then short-circuits.
9
+ * - Same-process concurrent callers dedupe through
10
+ * `inFlightByCacheDir` so they share one extraction promise.
11
+ *
12
+ * Per-archive staging:
13
+ *
14
+ * - The tar's inner directory is extracted into `${cacheDir}.staging/`
15
+ * first, then atomically renamed into `cacheDir`. Crucially, the
16
+ * staging path is keyed off `cacheDir` (not the tar's inner directory
17
+ * name) so two archives that happen to share an inner-dir name never
18
+ * collide on the same staging slot.
19
+ *
20
+ * Integrity guard:
21
+ *
22
+ * - After rename, we run the writer-side `Database.connect` so all
23
+ * migrations apply (column adds, new tables) and the version check
24
+ * fires before the ready marker is written. A corrupt or incompatible
25
+ * archive therefore poisons no cache entry — the cacheDir is removed
26
+ * and the next caller retries from scratch.
27
+ *
28
+ * TOCTOU guard:
29
+ *
30
+ * - The cache key is recomputed from the file's stat AFTER the untar
31
+ * completes. If the file changed mid-flight (a concurrent
32
+ * `crawl --append` rewrote it), the freshly-landed cache contents do
33
+ * not correspond to the original key, so we abort and remove the
34
+ * cacheDir so the caller upstream sees the inconsistency rather than
35
+ * serving misattributed data.
36
+ * @param archivePath - Absolute path to the source `.nitpicker` file.
37
+ * @param cacheRoot - Absolute path to the cache root directory.
38
+ * @param cacheDir - Absolute path the extracted contents should end up at.
39
+ * @param cacheKey - The cache key used to derive `cacheDir`. Recomputed
40
+ * after extraction to detect concurrent writers; must match.
41
+ * @returns Resolves once `cacheDir` is ready to be opened read-only.
42
+ */
43
+ export declare function extractArchiveToCache(archivePath: string, cacheRoot: string, cacheDir: string, cacheKey: string): Promise<void>;
@@ -0,0 +1,309 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { acquireArchiveLock, ArchiveLockError } from '../archive-lock.js';
4
+ import { Database } from '../database.js';
5
+ import { peekTarTopDir } from '../filesystem/peek-tar-top-dir.js';
6
+ import { rename } from '../filesystem/rename.js';
7
+ import { untar } from '../filesystem/untar.js';
8
+ import { IncompatibleArchiveError } from '../meta/types.js';
9
+ import { computeArchiveCacheKey } from './compute-archive-cache-key.js';
10
+ /**
11
+ * How long {@link extractArchiveToCache} waits in total for a peer
12
+ * extractor to finish (ms). 5 minutes is enough for the largest archives
13
+ * the crawler currently produces (~10 GB untars in ~10 s; 5 min gives
14
+ * 30× headroom) while still bounded so a deadlocked process eventually
15
+ * surfaces as a real error instead of hanging the viewer indefinitely.
16
+ */
17
+ const PEER_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
18
+ /**
19
+ * Polling interval while waiting on a peer extractor (ms). Short enough
20
+ * to keep latency low when the peer is fast, long enough to avoid
21
+ * burning CPU on a busy `fs.stat` loop.
22
+ */
23
+ const PEER_WAIT_POLL_MS = 50;
24
+ /**
25
+ * Marker file dropped into a cache directory only after the extraction
26
+ * has fully completed AND the contents have passed a structural sanity
27
+ * check (db.sqlite present, schema compatible, migrations applied).
28
+ *
29
+ * Internal — the file name is an implementation detail. Tests should
30
+ * assert through externally observable behaviour (cache hit on second
31
+ * open) rather than importing this constant.
32
+ */
33
+ const READY_MARKER = '.nitpicker-cache-ready';
34
+ /**
35
+ * In-process deduplication for concurrent {@link extractArchiveToCache}
36
+ * calls targeting the same `cacheDir`.
37
+ *
38
+ * The file-based {@link acquireArchiveLock} (mkdir-based) cannot defend
39
+ * against same-process race windows because the lock holder writes its
40
+ * `pid.txt` in a separate `await` from the directory creation: a sibling
41
+ * promise that observes the `EEXIST` between those two steps sees an
42
+ * empty `pid.txt`, mistakes the lock for stale, and clobbers it.
43
+ *
44
+ * Single-process dedup eliminates that race entirely for the common
45
+ * "viewer + MCP in the same process" topology while still letting the
46
+ * inner file lock guard cross-process collisions.
47
+ */
48
+ const inFlightByCacheDir = new Map();
49
+ /**
50
+ * Untar a `.nitpicker` archive into the given cache directory.
51
+ *
52
+ * Concurrency / re-entry contract:
53
+ *
54
+ * - Two viewers opening the same archive race for the cache lock; the
55
+ * loser waits and recheck-loops until the winner writes the ready
56
+ * marker, then short-circuits.
57
+ * - Same-process concurrent callers dedupe through
58
+ * `inFlightByCacheDir` so they share one extraction promise.
59
+ *
60
+ * Per-archive staging:
61
+ *
62
+ * - The tar's inner directory is extracted into `${cacheDir}.staging/`
63
+ * first, then atomically renamed into `cacheDir`. Crucially, the
64
+ * staging path is keyed off `cacheDir` (not the tar's inner directory
65
+ * name) so two archives that happen to share an inner-dir name never
66
+ * collide on the same staging slot.
67
+ *
68
+ * Integrity guard:
69
+ *
70
+ * - After rename, we run the writer-side `Database.connect` so all
71
+ * migrations apply (column adds, new tables) and the version check
72
+ * fires before the ready marker is written. A corrupt or incompatible
73
+ * archive therefore poisons no cache entry — the cacheDir is removed
74
+ * and the next caller retries from scratch.
75
+ *
76
+ * TOCTOU guard:
77
+ *
78
+ * - The cache key is recomputed from the file's stat AFTER the untar
79
+ * completes. If the file changed mid-flight (a concurrent
80
+ * `crawl --append` rewrote it), the freshly-landed cache contents do
81
+ * not correspond to the original key, so we abort and remove the
82
+ * cacheDir so the caller upstream sees the inconsistency rather than
83
+ * serving misattributed data.
84
+ * @param archivePath - Absolute path to the source `.nitpicker` file.
85
+ * @param cacheRoot - Absolute path to the cache root directory.
86
+ * @param cacheDir - Absolute path the extracted contents should end up at.
87
+ * @param cacheKey - The cache key used to derive `cacheDir`. Recomputed
88
+ * after extraction to detect concurrent writers; must match.
89
+ * @returns Resolves once `cacheDir` is ready to be opened read-only.
90
+ */
91
+ export async function extractArchiveToCache(archivePath, cacheRoot, cacheDir, cacheKey) {
92
+ if (await isCacheDirReady(cacheDir)) {
93
+ return;
94
+ }
95
+ const existing = inFlightByCacheDir.get(cacheDir);
96
+ if (existing) {
97
+ return existing;
98
+ }
99
+ const promise = runExtraction(archivePath, cacheRoot, cacheDir, cacheKey).finally(() => {
100
+ inFlightByCacheDir.delete(cacheDir);
101
+ });
102
+ inFlightByCacheDir.set(cacheDir, promise);
103
+ return promise;
104
+ }
105
+ /**
106
+ * The actual extraction work. Split out from {@link extractArchiveToCache}
107
+ * so the in-process dedup map can wrap it without disturbing the
108
+ * extraction flow itself.
109
+ * @param archivePath - Absolute path to the source `.nitpicker` file.
110
+ * @param cacheRoot - Absolute path to the cache root directory.
111
+ * @param cacheDir - Absolute path the extracted contents should end up at.
112
+ * @param cacheKey - Pre-extraction cache key, re-verified post-extraction.
113
+ */
114
+ async function runExtraction(archivePath, cacheRoot, cacheDir, cacheKey) {
115
+ await fs.mkdir(cacheRoot, { recursive: true });
116
+ const releaseLock = await acquireLockWithPeerWait(cacheDir);
117
+ try {
118
+ if (await isCacheDirReady(cacheDir)) {
119
+ return;
120
+ }
121
+ // Half-populated cacheDir recovery: another extractor crashed
122
+ // before writing the marker. Rename it aside instead of `rm`-ing
123
+ // in place so any reader that somehow still holds an fd on the
124
+ // old contents (the cache layer doesn't refcount readers) is not
125
+ // pulled out from under. The renamed quarantine dir is left for
126
+ // OS-level temp cleanup.
127
+ await quarantineHalfPopulatedCacheDir(cacheDir);
128
+ const stagingDir = `${cacheDir}.staging`;
129
+ // Stage clean: a previous crashed run may have left
130
+ // `<cacheDir>.staging/` around. `untar` would happily merge into
131
+ // it (with `newer:true` cherrypicking entries) so wipe first.
132
+ await fs.rm(stagingDir, { recursive: true, force: true });
133
+ await fs.mkdir(stagingDir, { recursive: true });
134
+ await untar(archivePath, { cwd: stagingDir });
135
+ // Concurrent-writer detection: if the source archive changed
136
+ // during our untar, the contents we just landed do NOT match the
137
+ // `cacheKey` that named the directory. Drop the staging dir and
138
+ // surface as an error so the caller (or a retry) can pick up the
139
+ // new key.
140
+ const postKey = await computeArchiveCacheKey(archivePath);
141
+ if (postKey !== cacheKey) {
142
+ await fs.rm(stagingDir, { recursive: true, force: true });
143
+ throw new Error(`Archive changed during cache extraction (key ${cacheKey} -> ${postKey}); ` +
144
+ `refusing to land mismatched contents at ${cacheDir}.`);
145
+ }
146
+ const innerDirName = await peekTarTopDir(archivePath);
147
+ const extractedInner = path.resolve(stagingDir, innerDirName);
148
+ // Move the inner directory into the cache slot. Any old quarantine
149
+ // is already aside, so the target is guaranteed empty.
150
+ await rename(extractedInner, cacheDir, true);
151
+ // The empty `stagingDir` wrapper is no longer useful — best effort
152
+ // remove (ignore errors, OS cleanup catches stragglers).
153
+ await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => { });
154
+ // Validate + migrate: open in WRITER mode so the full migration
155
+ // stack runs (initSchema / migrate*). On failure (db.sqlite
156
+ // missing, incompatible archive, broken schema), tear down the
157
+ // cache entry so the next caller does not get stuck on a
158
+ // "ready"-marked but broken cache.
159
+ try {
160
+ await runMigrationsOnCacheDir(cacheDir);
161
+ }
162
+ catch (error) {
163
+ await fs.rm(cacheDir, { recursive: true, force: true });
164
+ throw error;
165
+ }
166
+ await fs.writeFile(path.join(cacheDir, READY_MARKER), '', 'utf8');
167
+ }
168
+ finally {
169
+ await releaseLock();
170
+ }
171
+ }
172
+ /**
173
+ * Acquire the cache lock, blocking until any peer extractor finishes
174
+ * (rather than failing fast on `EEXIST` the way the writer-oriented
175
+ * {@link acquireArchiveLock} does). Returns the release function once
176
+ * the lock is held.
177
+ * @param cacheDir - Absolute path the lock guards.
178
+ */
179
+ async function acquireLockWithPeerWait(cacheDir) {
180
+ const startedAt = Date.now();
181
+ while (true) {
182
+ if (await isCacheDirReady(cacheDir)) {
183
+ // Peer finished and the marker is up. Return a no-op release
184
+ // so the caller can `finally`-await it without branching.
185
+ return async () => { };
186
+ }
187
+ try {
188
+ return await acquireArchiveLock(cacheDir);
189
+ }
190
+ catch (error) {
191
+ if (!(error instanceof ArchiveLockError)) {
192
+ throw error;
193
+ }
194
+ if (Date.now() - startedAt > PEER_WAIT_TIMEOUT_MS) {
195
+ throw error;
196
+ }
197
+ await sleep(PEER_WAIT_POLL_MS);
198
+ }
199
+ }
200
+ }
201
+ /**
202
+ * Open the migrated DB in writer mode so all migrations apply, then
203
+ * release the handle. Cache entries land migrated, so the subsequent
204
+ * `Archive.connect` read-only open never needs to mutate the cache dir
205
+ * (and never silently misses a newly-added column).
206
+ * @param cacheDir - Absolute path to the freshly-extracted cache dir.
207
+ */
208
+ async function runMigrationsOnCacheDir(cacheDir) {
209
+ const dbPath = path.join(cacheDir, 'db.sqlite');
210
+ if (!(await fileExists(dbPath))) {
211
+ throw new Error(`Cache directory does not contain db.sqlite: ${cacheDir}`);
212
+ }
213
+ const db = await Database.connect({ filename: dbPath, readOnly: false });
214
+ try {
215
+ // `Database.connect` ran migrations during init. Closing here
216
+ // flushes WAL + drops the handle so the read-only re-open in
217
+ // `Archive.connect` does not race the writer connection.
218
+ await db.destroy();
219
+ }
220
+ catch (error) {
221
+ // Propagate after attempting cleanup of the connection (best
222
+ // effort — the cache dir tear-down happens in the caller).
223
+ if (error instanceof IncompatibleArchiveError) {
224
+ throw error;
225
+ }
226
+ throw error;
227
+ }
228
+ }
229
+ /**
230
+ * Move a half-populated cacheDir aside so the next extraction can land
231
+ * cleanly without yanking live readers' files out from under them. The
232
+ * quarantine path gets a unique suffix so two crashes in succession
233
+ * don't collide.
234
+ * @param cacheDir - Absolute path of the cache slot to free.
235
+ */
236
+ async function quarantineHalfPopulatedCacheDir(cacheDir) {
237
+ try {
238
+ await fs.access(cacheDir);
239
+ }
240
+ catch {
241
+ // Nothing to quarantine.
242
+ return;
243
+ }
244
+ if (await isCacheDirReady(cacheDir)) {
245
+ // Already settled — caller will short-circuit, no quarantine needed.
246
+ return;
247
+ }
248
+ const quarantinePath = `${cacheDir}.corrupt.${process.pid}.${nextQuarantineCounter()}`;
249
+ try {
250
+ await fs.rename(cacheDir, quarantinePath);
251
+ }
252
+ catch {
253
+ // If rename fails (e.g. cross-volume), fall back to direct
254
+ // removal — accepting the rm-while-reader risk this single time
255
+ // since we have no other option.
256
+ await fs.rm(cacheDir, { recursive: true, force: true });
257
+ }
258
+ }
259
+ /**
260
+ * Monotonic counter appended to quarantine directory names so multiple
261
+ * recoveries in a single process never collide on the same path. Resets
262
+ * to zero after `2 ** 32` increments; that bound is far larger than any
263
+ * realistic per-process quarantine count.
264
+ */
265
+ let quarantineCounter = 0;
266
+ /**
267
+ * Issue the next quarantine sequence number.
268
+ * @returns A non-negative integer, monotonically increasing per process.
269
+ */
270
+ function nextQuarantineCounter() {
271
+ quarantineCounter = (quarantineCounter + 1) % 0x1_00_00_00_00;
272
+ return quarantineCounter;
273
+ }
274
+ /**
275
+ * Async existence probe — avoids blocking the event loop on the warm
276
+ * cache-hit fast path.
277
+ * @param targetPath - Absolute path to probe.
278
+ * @returns `true` if the path is reachable via `fs.access`.
279
+ */
280
+ async function fileExists(targetPath) {
281
+ try {
282
+ await fs.access(targetPath);
283
+ return true;
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ /**
290
+ * Whether a cache directory contains a fully-validated extraction. The
291
+ * READY marker is the only trustworthy signal — sibling files alone
292
+ * cannot be trusted because a half-extracted dir can also contain a
293
+ * `db.sqlite`.
294
+ * @param cacheDir - Absolute path to a candidate cache directory.
295
+ * @returns `true` if the marker exists.
296
+ */
297
+ async function isCacheDirReady(cacheDir) {
298
+ return await fileExists(path.join(cacheDir, READY_MARKER));
299
+ }
300
+ /**
301
+ * Promise-based sleep used by the peer-wait poll. Kept inline so the
302
+ * file has no external sleep dependency and so the implementation can
303
+ * later swap for an AbortSignal-aware version without touching the
304
+ * call site.
305
+ * @param ms - Milliseconds to wait.
306
+ */
307
+ function sleep(ms) {
308
+ return new Promise((resolve) => setTimeout(resolve, ms));
309
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve the directory where extracted `.nitpicker` archives are cached.
3
+ *
4
+ * Resolution order:
5
+ *
6
+ * 1. `NITPICKER_TAR_CACHE_DIR` env — explicit override (CI, testing, or
7
+ * operators who want the cache on a specific volume). Must be a
8
+ * path; values that look like boolean / sentinel words (e.g. `0`,
9
+ * `false`) are ignored to keep the cache from landing somewhere
10
+ * surprising when the user mistakes the env contract.
11
+ * 2. `<os.tmpdir()>/nitpicker/cache/` — default. Lives under the OS
12
+ * temp directory so the platform's own cleanup (macOS reboot, Linux
13
+ * `systemd-tmpfiles`, Windows Disk Cleanup) reclaims stale entries
14
+ * without bespoke logic on our side.
15
+ *
16
+ * The returned path is absolute. The caller is responsible for creating
17
+ * it on demand (this function is pure).
18
+ * @returns Absolute path to the cache root directory.
19
+ */
20
+ export declare function getArchiveCacheRoot(): string;
@@ -0,0 +1,53 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /**
4
+ * Env values that look like a "disable" sentinel rather than a path.
5
+ *
6
+ * A user who copy-pastes the `NITPICKER_DISABLE_TAR_CACHE` convention
7
+ * onto `NITPICKER_TAR_CACHE_DIR` (e.g. `NITPICKER_TAR_CACHE_DIR=0`
8
+ * thinking it means "use default") would otherwise silently land the
9
+ * cache at `$PWD/0/` — multi-gigabyte extracts polluting the project
10
+ * tree, never reclaimed by OS temp cleanup. Reject these explicitly
11
+ * and fall back to the default location instead.
12
+ */
13
+ const SENTINEL_LIKE_OVERRIDES = new Set([
14
+ '0',
15
+ '1',
16
+ 'false',
17
+ 'true',
18
+ 'no',
19
+ 'yes',
20
+ 'off',
21
+ 'on',
22
+ 'null',
23
+ 'undefined',
24
+ ]);
25
+ /**
26
+ * Resolve the directory where extracted `.nitpicker` archives are cached.
27
+ *
28
+ * Resolution order:
29
+ *
30
+ * 1. `NITPICKER_TAR_CACHE_DIR` env — explicit override (CI, testing, or
31
+ * operators who want the cache on a specific volume). Must be a
32
+ * path; values that look like boolean / sentinel words (e.g. `0`,
33
+ * `false`) are ignored to keep the cache from landing somewhere
34
+ * surprising when the user mistakes the env contract.
35
+ * 2. `<os.tmpdir()>/nitpicker/cache/` — default. Lives under the OS
36
+ * temp directory so the platform's own cleanup (macOS reboot, Linux
37
+ * `systemd-tmpfiles`, Windows Disk Cleanup) reclaims stale entries
38
+ * without bespoke logic on our side.
39
+ *
40
+ * The returned path is absolute. The caller is responsible for creating
41
+ * it on demand (this function is pure).
42
+ * @returns Absolute path to the cache root directory.
43
+ */
44
+ export function getArchiveCacheRoot() {
45
+ const envOverride = process.env.NITPICKER_TAR_CACHE_DIR;
46
+ if (envOverride && envOverride.trim().length > 0) {
47
+ const trimmed = envOverride.trim();
48
+ if (!SENTINEL_LIKE_OVERRIDES.has(trimmed.toLowerCase())) {
49
+ return path.resolve(trimmed);
50
+ }
51
+ }
52
+ return path.resolve(os.tmpdir(), 'nitpicker', 'cache');
53
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Whether the tar cache is disabled by the `NITPICKER_DISABLE_TAR_CACHE`
3
+ * env var.
4
+ *
5
+ * Useful for two scenarios:
6
+ *
7
+ * 1. Debugging — bypass the cache to reproduce the cold-start behaviour
8
+ * against a fresh tmpDir.
9
+ * 2. Sandboxed CI — when the runner's tmpfs would not survive between
10
+ * steps anyway, the cache only adds first-step overhead.
11
+ *
12
+ * Accepted truthy values: `1`, `true`, `yes`, `on` (case-insensitive).
13
+ * Anything else (including unset) keeps the cache enabled.
14
+ * @returns `true` when the cache should be bypassed.
15
+ * @example
16
+ * ```ts
17
+ * if (isArchiveCacheDisabled()) {
18
+ * // Fall back to the writer path; cwd tmpDir + close-time cleanup.
19
+ * const archive = await Archive.open({ filePath });
20
+ * ...
21
+ * }
22
+ * ```
23
+ */
24
+ export declare function isArchiveCacheDisabled(): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Whether the tar cache is disabled by the `NITPICKER_DISABLE_TAR_CACHE`
3
+ * env var.
4
+ *
5
+ * Useful for two scenarios:
6
+ *
7
+ * 1. Debugging — bypass the cache to reproduce the cold-start behaviour
8
+ * against a fresh tmpDir.
9
+ * 2. Sandboxed CI — when the runner's tmpfs would not survive between
10
+ * steps anyway, the cache only adds first-step overhead.
11
+ *
12
+ * Accepted truthy values: `1`, `true`, `yes`, `on` (case-insensitive).
13
+ * Anything else (including unset) keeps the cache enabled.
14
+ * @returns `true` when the cache should be bypassed.
15
+ * @example
16
+ * ```ts
17
+ * if (isArchiveCacheDisabled()) {
18
+ * // Fall back to the writer path; cwd tmpDir + close-time cleanup.
19
+ * const archive = await Archive.open({ filePath });
20
+ * ...
21
+ * }
22
+ * ```
23
+ */
24
+ export function isArchiveCacheDisabled() {
25
+ const raw = process.env.NITPICKER_DISABLE_TAR_CACHE;
26
+ if (!raw) {
27
+ return false;
28
+ }
29
+ const normalized = raw.trim().toLowerCase();
30
+ return (normalized === '1' ||
31
+ normalized === 'true' ||
32
+ normalized === 'yes' ||
33
+ normalized === 'on');
34
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Build the absolute path to a single archive's cache directory.
3
+ *
4
+ * The directory name is `<cacheKey>-<safeBasename>`:
5
+ *
6
+ * - `cacheKey` carries the freshness signal (size/mtime/ctime) and is
7
+ * what actually drives cache hit vs miss.
8
+ * - `safeBasename` is appended purely so a human running `ls` on the
9
+ * cache root can recognise which archive an entry belongs to. It is
10
+ * NOT used to disambiguate keys — two archives with identical inode
11
+ * metadata (e.g. an identical copy under a different name) intentionally
12
+ * share an entry under the first basename that landed there.
13
+ *
14
+ * The basename is sanitised: anything outside `[A-Za-z0-9._-]` becomes
15
+ * `_`. This keeps the path portable across filesystems (no spaces,
16
+ * unicode normalisation surprises, Windows-reserved chars) and removes
17
+ * any chance that a crafted archive name could escape the cache root
18
+ * (e.g. via `..` or path separators), independent of the upstream
19
+ * `path.basename` call that already drops directory components.
20
+ * @param cacheRoot - Absolute path returned by `getArchiveCacheRoot()`.
21
+ * @param cacheKey - The freshness key from `computeArchiveCacheKey()`.
22
+ * @param archivePath - Absolute path to the source `.nitpicker` file;
23
+ * only its basename contributes to the cache entry name.
24
+ * @returns Absolute path to the per-archive cache directory.
25
+ */
26
+ export declare function resolveArchiveCacheDir(cacheRoot: string, cacheKey: string, archivePath: string): string;
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Build the absolute path to a single archive's cache directory.
4
+ *
5
+ * The directory name is `<cacheKey>-<safeBasename>`:
6
+ *
7
+ * - `cacheKey` carries the freshness signal (size/mtime/ctime) and is
8
+ * what actually drives cache hit vs miss.
9
+ * - `safeBasename` is appended purely so a human running `ls` on the
10
+ * cache root can recognise which archive an entry belongs to. It is
11
+ * NOT used to disambiguate keys — two archives with identical inode
12
+ * metadata (e.g. an identical copy under a different name) intentionally
13
+ * share an entry under the first basename that landed there.
14
+ *
15
+ * The basename is sanitised: anything outside `[A-Za-z0-9._-]` becomes
16
+ * `_`. This keeps the path portable across filesystems (no spaces,
17
+ * unicode normalisation surprises, Windows-reserved chars) and removes
18
+ * any chance that a crafted archive name could escape the cache root
19
+ * (e.g. via `..` or path separators), independent of the upstream
20
+ * `path.basename` call that already drops directory components.
21
+ * @param cacheRoot - Absolute path returned by `getArchiveCacheRoot()`.
22
+ * @param cacheKey - The freshness key from `computeArchiveCacheKey()`.
23
+ * @param archivePath - Absolute path to the source `.nitpicker` file;
24
+ * only its basename contributes to the cache entry name.
25
+ * @returns Absolute path to the per-archive cache directory.
26
+ */
27
+ export function resolveArchiveCacheDir(cacheRoot, cacheKey, archivePath) {
28
+ const rawBasename = path.basename(archivePath, path.extname(archivePath));
29
+ const safeBasename = rawBasename.replaceAll(/[^\w.-]+/g, '_').slice(0, 80);
30
+ const dirName = safeBasename.length > 0 ? `${cacheKey}-${safeBasename}` : cacheKey;
31
+ return path.resolve(cacheRoot, dirName);
32
+ }