website-to-markdown 1.0.1 → 1.1.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.
@@ -3,6 +3,12 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
3
  import { resolve } from 'node:path'
4
4
  import { sha256Hex } from './hash.js'
5
5
  import type { Logger } from './logger.js'
6
+ import {
7
+ CRAWL_SNAPSHOT_NAME_PATTERN,
8
+ type CrawlSnapshotInput
9
+ } from './schema/index.js'
10
+
11
+ export { CRAWL_SNAPSHOT_NAME_PATTERN, type CrawlSnapshotInput }
6
12
 
7
13
  /**
8
14
  * A crawl recorded once and replayed, for benchmarks. Two runs of a pipeline
@@ -19,32 +25,24 @@ import type { Logger } from './logger.js'
19
25
  * other tenants' jobs on the same lane. Never in production.
20
26
  */
21
27
 
22
- export type CrawlSnapshotInput = {
23
- mode: 'record' | 'replay'
24
- /** `<snapshot>/<host>`: the bench's snapshot label, then the site. */
25
- name: string
26
- }
27
-
28
- /** The name becomes a path, so it may not climb out of `root`. */
29
- export const CRAWL_SNAPSHOT_NAME_PATTERN =
30
- /^[a-z0-9._-]{1,80}\/[a-z0-9._-]{1,120}$/
31
-
32
28
  type CrawlSnapshot = CrawlSnapshotInput & {
33
29
  directory: string
34
30
  logger: Logger | undefined
35
31
  }
36
32
 
33
+ export type CrawlSnapshotOptions = {
34
+ /** Directory the snapshots live under. */
35
+ root: string
36
+ /** Told about replay misses. */
37
+ logger?: Logger | undefined
38
+ }
39
+
37
40
  const snapshot = new AsyncLocalStorage<CrawlSnapshot>()
38
41
 
39
42
  export function withCrawlSnapshot<T>(
40
43
  input: CrawlSnapshotInput | null | undefined,
41
44
  run: () => Promise<T>,
42
- options: {
43
- /** Directory the snapshots live under. */
44
- root: string
45
- /** Told about replay misses. */
46
- logger?: Logger
47
- }
45
+ options: CrawlSnapshotOptions
48
46
  ) {
49
47
  if (!input) return run()
50
48
  if (process.env.NODE_ENV === 'production') {
package/src/fetch-page.ts CHANGED
@@ -10,7 +10,13 @@ import {
10
10
  type ContentKind,
11
11
  convertibleDocumentType
12
12
  } from './content-type.js'
13
- import { currentCrawlSnapshot, throughCrawlSnapshot } from './crawl-snapshot.js'
13
+ import {
14
+ type CrawlSnapshotInput,
15
+ type CrawlSnapshotOptions,
16
+ currentCrawlSnapshot,
17
+ throughCrawlSnapshot,
18
+ withCrawlSnapshot
19
+ } from './crawl-snapshot.js'
14
20
  import {
15
21
  isApplicationShell,
16
22
  isBlankMarkdown,
@@ -473,7 +479,18 @@ export type WebsiteToMarkdownOptions = {
473
479
  createLimit?: CreateConcurrencyLimit
474
480
  /** Workers AI client errors other than 429s, for an error tracker. */
475
481
  onApiError?: (error: ApiError) => void
482
+ /**
483
+ * Pass Workers AI timeouts to `onApiError`. A timed-out page falls through
484
+ * to the next tier, so `false` keeps import bursts out of error tracking.
485
+ * Default true.
486
+ */
487
+ reportTimeouts?: boolean
476
488
  logger?: Logger
489
+ /**
490
+ * Where the returned `withCrawlSnapshot` keeps snapshots. Its logger
491
+ * defaults to `logger`.
492
+ */
493
+ crawlSnapshot?: CrawlSnapshotOptions
477
494
  /** The OCR tier for a PDF Workers AI returned nothing for. */
478
495
  parsePdf?: PdfParser
479
496
  /**
@@ -494,6 +511,7 @@ export function createWebsiteToMarkdown(options: WebsiteToMarkdownOptions) {
494
511
  credentials: options.cloudflare,
495
512
  createLimit,
496
513
  onApiError: options.onApiError,
514
+ reportTimeouts: options.reportTimeouts,
497
515
  logger
498
516
  })
499
517
  const hostLimits = createHostLimits(createLimit)
@@ -1003,7 +1021,23 @@ export function createWebsiteToMarkdown(options: WebsiteToMarkdownOptions) {
1003
1021
  convertDocumentBytes,
1004
1022
  urlToMarkdown,
1005
1023
  convertFileWithWorkersAi: client.convertFileWithWorkersAi,
1006
- renderUrlToMarkdown: client.renderUrlToMarkdown
1024
+ renderUrlToMarkdown: client.renderUrlToMarkdown,
1025
+ /** The SSRF guard the converter runs, bound to its `logger`. */
1026
+ isHostAllowed: allowHost,
1027
+ /** `withCrawlSnapshot` bound to the `crawlSnapshot` option. */
1028
+ withCrawlSnapshot<T>(
1029
+ input: CrawlSnapshotInput | null | undefined,
1030
+ run: () => Promise<T>
1031
+ ) {
1032
+ if (!input) return run()
1033
+ if (!options.crawlSnapshot) {
1034
+ throw new Error('Set crawlSnapshot.root to use crawl snapshots')
1035
+ }
1036
+ return withCrawlSnapshot(input, run, {
1037
+ root: options.crawlSnapshot.root,
1038
+ logger: options.crawlSnapshot.logger ?? logger
1039
+ })
1040
+ }
1007
1041
  }
1008
1042
  }
1009
1043
 
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  export {
12
12
  CRAWL_SNAPSHOT_NAME_PATTERN,
13
13
  type CrawlSnapshotInput,
14
+ type CrawlSnapshotOptions,
14
15
  currentCrawlSnapshot,
15
16
  throughCrawlSnapshot,
16
17
  withCrawlSnapshot
package/src/limit.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createMemoryConcurrencyLimit } from './memory-limit.js'
2
2
 
3
3
  /**
4
- * A semaphore: at most `max` callbacks in flight. hellu's Redis Lua limiter
5
- * (`createRedisConcurrencyLimit`) fits this shape as is; the default is
4
+ * A semaphore: at most `max` callbacks in flight. `hanzio/redis`'s
5
+ * `createRedisConcurrencyLimit` returns this shape (plus `queueDepth`, which is
6
+ * not needed here) once `send` is bound; the default is
6
7
  * {@link createMemoryConcurrencyLimit}.
7
8
  */
8
9
  export type ConcurrencyLimit = {
@@ -0,0 +1,16 @@
1
+ import {
2
+ type ApiError,
3
+ HttpResponseError,
4
+ RequestTimeoutError
5
+ } from 'hanzio/api-wrapper'
6
+
7
+ /**
8
+ * Whether `onApiError` hears about a Workers AI client error. A 429 never: it
9
+ * is retried and trips the shared cooldown, and reporting every one floods the
10
+ * error tracker. A timeout only with `reportTimeouts`, since the page falls
11
+ * through to the next tier anyway.
12
+ */
13
+ export function isReportableApiError(error: ApiError, reportTimeouts = true) {
14
+ if (error instanceof HttpResponseError && error.status === 429) return false
15
+ return reportTimeouts || !(error instanceof RequestTimeoutError)
16
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Values a shared schema can import without the runtime: no Node built-ins,
3
+ * no peer libraries.
4
+ */
5
+
6
+ /** A crawl snapshot name, `<snapshot>/<host>`; it becomes a path, so it may not climb out of the root. */
7
+ export const CRAWL_SNAPSHOT_NAME_PATTERN =
8
+ /^[a-z0-9._-]{1,80}\/[a-z0-9._-]{1,120}$/
9
+
10
+ export type CrawlSnapshotInput = {
11
+ mode: 'record' | 'replay'
12
+ /** `<snapshot>/<host>`: the bench's snapshot label, then the site. */
13
+ name: string
14
+ }