polite-crawler 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +28 -121
  2. package/build/{browser-CKRNbIDT.mjs → browser-56dBBLec.mjs} +7 -3
  3. package/build/browser-56dBBLec.mjs.map +1 -0
  4. package/build/classify.d.mts +50 -2
  5. package/build/{cli-daemon-ZYRdy6EW.mjs → cli-daemon-qS4PH2bM.mjs} +2 -2
  6. package/build/{cli-daemon-ZYRdy6EW.mjs.map → cli-daemon-qS4PH2bM.mjs.map} +1 -1
  7. package/build/{cli-recipes-Bci6HREA.mjs → cli-recipes-qbLRVThF.mjs} +2 -2
  8. package/build/{cli-recipes-Bci6HREA.mjs.map → cli-recipes-qbLRVThF.mjs.map} +1 -1
  9. package/build/cli.mjs +41 -16
  10. package/build/cli.mjs.map +1 -1
  11. package/build/{daemon-DK6U51_v.mjs → daemon-BXBbUrni.mjs} +11 -7
  12. package/build/daemon-BXBbUrni.mjs.map +1 -0
  13. package/build/{diff-BUM91Rev.mjs → diff-CpKN0Jdc.mjs} +7 -3
  14. package/build/{diff-BUM91Rev.mjs.map → diff-CpKN0Jdc.mjs.map} +1 -1
  15. package/build/{extract-CefB5Vfn.mjs → extract-DqeqiNHb.mjs} +12 -4
  16. package/build/extract-DqeqiNHb.mjs.map +1 -0
  17. package/build/{html-BACljlkf.d.mts → html-DMgIeQfJ.d.mts} +16 -2
  18. package/build/html.d.mts +1 -2
  19. package/build/index.d.mts +4227 -40
  20. package/build/index.mjs +17 -17
  21. package/build/{log-Dx-wXjJl.mjs → log-CmBWcyQm.mjs} +5 -1
  22. package/build/log-CmBWcyQm.mjs.map +1 -0
  23. package/build/mcp-package-Y0bREnOS.mjs +40 -0
  24. package/build/mcp-package-Y0bREnOS.mjs.map +1 -0
  25. package/build/{ollama-BJxkjny4.mjs → ollama-D1xRh3hY.mjs} +7 -3
  26. package/build/ollama-D1xRh3hY.mjs.map +1 -0
  27. package/build/{open-CbpSCEZc.mjs → open-CEo22Hpu.mjs} +78 -18
  28. package/build/open-CEo22Hpu.mjs.map +1 -0
  29. package/build/politeness.d.mts +366 -2
  30. package/build/{recipe-GiFQojmo.mjs → recipe-B2SkepDZ.mjs} +10 -4
  31. package/build/recipe-B2SkepDZ.mjs.map +1 -0
  32. package/build/{recipe-product-Dellqudu.mjs → recipe-product-CyZE3Ukd.mjs} +29 -7
  33. package/build/recipe-product-CyZE3Ukd.mjs.map +1 -0
  34. package/build/{render-C1CUraWF.mjs → render-CKE2LILH.mjs} +10 -3
  35. package/build/render-CKE2LILH.mjs.map +1 -0
  36. package/build/robots.d.mts +143 -2
  37. package/build/robots.mjs +1 -1
  38. package/build/robots.mjs.map +1 -1
  39. package/build/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-8H4AJuhK.mjs} +1 -0
  40. package/build/{runtime-D_r2SYge.mjs → runtime-C_2BQibH.mjs} +84 -18
  41. package/build/runtime-C_2BQibH.mjs.map +1 -0
  42. package/build/signals.d.mts +45 -2
  43. package/build/{transformers-BQMlaGj4.mjs → transformers-BNiuiLxT.mjs} +3 -3
  44. package/build/transformers-BNiuiLxT.mjs.map +1 -0
  45. package/build/{version-BPem-XZd.mjs → version-Djva7JkN.mjs} +7 -2
  46. package/build/version-Djva7JkN.mjs.map +1 -0
  47. package/build/{wacz-QyzLp4mC.mjs → wacz-DKk2HBmx.mjs} +4 -4
  48. package/build/{wacz-QyzLp4mC.mjs.map → wacz-DKk2HBmx.mjs.map} +1 -1
  49. package/build/{warc-BHcaRKHw.mjs → warc-C_ew76bV.mjs} +4 -4
  50. package/build/{warc-BHcaRKHw.mjs.map → warc-C_ew76bV.mjs.map} +1 -1
  51. package/build/{webbotauth-CjJdTAfE.mjs → webbotauth-Dq_dA_u3.mjs} +2 -2
  52. package/build/{webbotauth-CjJdTAfE.mjs.map → webbotauth-Dq_dA_u3.mjs.map} +1 -1
  53. package/package.json +5 -11
  54. package/build/browser-CKRNbIDT.mjs.map +0 -1
  55. package/build/classify-CrfyXcwo.d.mts +0 -51
  56. package/build/daemon-DK6U51_v.mjs.map +0 -1
  57. package/build/daemon-wwIWsUmU.d.mts +0 -3777
  58. package/build/extract-CefB5Vfn.mjs.map +0 -1
  59. package/build/log-Dx-wXjJl.mjs.map +0 -1
  60. package/build/loopback-C_QYowSf.mjs +0 -22
  61. package/build/loopback-C_QYowSf.mjs.map +0 -1
  62. package/build/mcp.d.mts +0 -199
  63. package/build/mcp.mjs +0 -1555
  64. package/build/mcp.mjs.map +0 -1
  65. package/build/ollama-BJxkjny4.mjs.map +0 -1
  66. package/build/open-CbpSCEZc.mjs.map +0 -1
  67. package/build/politeness-yI3xBEx9.d.mts +0 -367
  68. package/build/recipe-GiFQojmo.mjs.map +0 -1
  69. package/build/recipe-product-Dellqudu.mjs.map +0 -1
  70. package/build/render-C1CUraWF.mjs.map +0 -1
  71. package/build/robots-R-qaEBOb.d.mts +0 -144
  72. package/build/runtime-D_r2SYge.mjs.map +0 -1
  73. package/build/signals-BHpy2xb2.d.mts +0 -46
  74. package/build/transformers-BQMlaGj4.mjs.map +0 -1
  75. package/build/urls-BTDSgDLC.d.mts +0 -16
  76. package/build/version-BPem-XZd.mjs.map +0 -1
@@ -1,2 +1,366 @@
1
- import { A as validatorsToSend, C as backoffMs, D as policyFor, E as nextValidatorMemo, O as realClock, S as ValidatorVerdict, T as equalJitterMs, _ as VALIDATOR_MEMO, a as HostScheduler, b as ValidatorMemo, c as HostStopped, d as Paced, f as PolicyOptions, g as SchedulerOptions, h as RestInput, i as HostResult, k as restAfter, l as MAX_STOP_MS, m as Rest, n as DEFAULT_POLICY, o as HostStateStore, p as PolitenessPolicy, r as HostCounts, s as HostStop, t as Clock, u as MemoryHostStateStore, v as ValidatorAnswer, w as checkPolicy, x as ValidatorReport, y as ValidatorLimits } from "./politeness-yI3xBEx9.mjs";
2
- export { Clock, DEFAULT_POLICY, HostCounts, HostResult, HostScheduler, HostStateStore, HostStop, HostStopped, MAX_STOP_MS, MemoryHostStateStore, Paced, PolicyOptions, PolitenessPolicy, Rest, RestInput, SchedulerOptions, VALIDATOR_MEMO, ValidatorAnswer, ValidatorLimits, ValidatorMemo, ValidatorReport, ValidatorVerdict, backoffMs, checkPolicy, equalJitterMs, nextValidatorMemo, policyFor, realClock, restAfter, validatorsToSend };
1
+ //#region src/politeness.d.ts
2
+ /**
3
+ * The scheduler controls the pace of each host. The speed of a polite crawler is the number of
4
+ * hosts in progress divided by the delay for each host. Thus the scheduler keeps one request in
5
+ * progress for each host, and it waits the delay of the host between two requests. It uses its
6
+ * concurrency across many hosts, as the back queues of the Mercator crawler do.
7
+ *
8
+ * The caller decides the delay after each request, and `run` gets it from the task. Only the caller
9
+ * knows the documented rate of the source, the crawl delay of the site and the duration of the
10
+ * request. A wait holds no global slot, so a host that waits does not block a different host.
11
+ *
12
+ * The state of each host (its next time, its counts and its stop) is in a `HostStateStore`. The
13
+ * default store is in memory. A store that processes share keeps one pace for each host across them.
14
+ */
15
+ export interface Clock {
16
+ readonly now: () => number;
17
+ /** This function waits `ms` milliseconds. When the signal aborts, the wait ends early. */
18
+ readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
19
+ }
20
+ export declare const realClock: Clock;
21
+ /** The crawler does not ask a host again until the stop ends, after a refusal (a challenge, 403 or 402). */
22
+ export declare class HostStopped extends Error {
23
+ readonly host: string;
24
+ readonly reason: string;
25
+ /** This value is the end of the stop, in milliseconds since the epoch. `Infinity` is a stop with no end. */
26
+ readonly until: number;
27
+ constructor(host: string, reason: string, until?: number);
28
+ }
29
+ export interface Paced<T> {
30
+ readonly value: T;
31
+ /** This value is the time that the host rests before its next request. */
32
+ readonly delayMs: number;
33
+ }
34
+ /** A stop keeps the crawler away from a host until a time. */
35
+ export interface HostStop {
36
+ /** This value is the end of the stop, in milliseconds since the epoch. `Infinity` is a stop with no end. */
37
+ readonly until: number;
38
+ readonly reason: string;
39
+ }
40
+ /** These are the counts of a host. Each count gives the results of one kind in sequence. */
41
+ export interface HostCounts {
42
+ /** This value counts errors and requests with no answer. At the limit of the policy, the host stops. */
43
+ readonly failures: number;
44
+ /** This value counts the requests to wait (429, 502, 503, 504). It sets the backoff and does not stop the host. */
45
+ readonly waits: number;
46
+ }
47
+ /**
48
+ * This value tells how one result changes the counts of a host:
49
+ *
50
+ * - `success` sets the two counts to zero.
51
+ * - `failure` adds one to `failures`.
52
+ * - `wait` adds one to `waits`.
53
+ * - `neutral` changes nothing. A client error (a 400) is neutral, because a different request can succeed.
54
+ */
55
+ export type HostResult = "success" | "failure" | "wait" | "neutral";
56
+ /**
57
+ * A host state store keeps the state of each host for the scheduler. Its methods are synchronous,
58
+ * and each method is one atomic step on the state of one host. Thus a store can be a table of
59
+ * SQLite that processes share. Times are milliseconds since the epoch.
60
+ *
61
+ * The state of a host has five parts:
62
+ *
63
+ * - the next time: the earliest time of the next request
64
+ * - the lease: the end of the slot of the request in progress, when there is one
65
+ * - the counts (`HostCounts`)
66
+ * - the stop: an end time and a reason, when there is one
67
+ */
68
+ export interface HostStateStore {
69
+ /**
70
+ * This method asks for the slot of a host at the time `now`. When the host has no rest left
71
+ * and no lease, the store gives the slot to the caller until `now + leaseMs` and gives 0.
72
+ * Otherwise it changes nothing and gives the time to wait in milliseconds: the time to the next
73
+ * time, or to the end of the lease. A store that processes share gives a short wait (one second
74
+ * or less) during a lease, because the holder usually releases it early. The caller asks again after the wait.
75
+ */
76
+ reserve(host: string, now: number, leaseMs: number): number;
77
+ /** This method changes the counts of a host for one result, and it gives the new counts. */
78
+ record(host: string, result: HostResult): HostCounts;
79
+ /**
80
+ * This method ends the lease of the caller. It sets the next time to `nextAt` when `nextAt` is
81
+ * later than the stored next time. Only the holder of the lease uses it.
82
+ */
83
+ release(host: string, nextAt: number): void;
84
+ /** This method sets the next time to `until` when `until` is later. It does not touch the lease. */
85
+ defer(host: string, until: number): void;
86
+ /** This method stops a host until a time. When the host has a stop that ends later, that stop stays. */
87
+ stop(host: string, until: number, reason: string): void;
88
+ /** This method gives the stop of a host at the time `now`. A stop that ended gives no value. */
89
+ stopped(host: string, now: number): HostStop | undefined;
90
+ /**
91
+ * This method removes the stops and the counts of one host, or of each host when there is no
92
+ * host. It keeps the next times and the leases, so a reset cannot make a host go faster than its pace.
93
+ */
94
+ reset(host?: string): void;
95
+ /**
96
+ * This method gives the validator memo of a host, when the store has one. A store without this
97
+ * method and `noteValidators` has no memo. The crawler then sends each validator of a stored copy.
98
+ */
99
+ validators?(host: string): ValidatorMemo | undefined;
100
+ /**
101
+ * This method adds one conditional request to the memo of a host, with the rules of
102
+ * `nextValidatorMemo`, in one atomic step. It gives the new memo.
103
+ */
104
+ noteValidators?(host: string, report: ValidatorReport, now: number): ValidatorMemo;
105
+ }
106
+ /** This value tells the result that the crawler learned for one validator of a host. */
107
+ export type ValidatorVerdict = "unknown" | "works" | "fails";
108
+ /**
109
+ * This value tells what one conditional request got:
110
+ *
111
+ * - `not-modified`: a 304.
112
+ * - `same`: a 200 with the body that the store has. Thus the validator did not match.
113
+ * - `changed`: a 200 with a different body. This answer tells nothing about the validator.
114
+ */
115
+ export type ValidatorAnswer = "not-modified" | "same" | "changed";
116
+ /** This report tells the validators that one conditional request sent, and the answer that it got. */
117
+ export interface ValidatorReport {
118
+ /** This value is true when the request sent `If-None-Match`. */
119
+ readonly etag: boolean;
120
+ /** This value is true when the request sent `If-Modified-Since`. */
121
+ readonly lastModified: boolean;
122
+ readonly answer: ValidatorAnswer;
123
+ }
124
+ /**
125
+ * A validator memo holds what the crawler learned about the validators of one host. A server
126
+ * examines `If-None-Match` first, and it ignores `If-Modified-Since` when the request has the two
127
+ * (RFC 9110, section 13.1.3). Thus a host with unstable ETags does not answer 304 to a request with
128
+ * the two validators at any time. Its stable `Last-Modified` date does not help. `nextValidatorMemo`
129
+ * gives the rules.
130
+ */
131
+ export interface ValidatorMemo {
132
+ /** This value is true when the crawler leaves out `If-None-Match` for the host and sends only `If-Modified-Since`. */
133
+ readonly skipEtag: boolean;
134
+ /** This value tells if a request with `If-None-Match` got a 304 (`works`) or the stored body again (`fails`). */
135
+ readonly etag: ValidatorVerdict;
136
+ /** This value tells the same for a request with only `If-Modified-Since`. */
137
+ readonly lastModified: ValidatorVerdict;
138
+ /** This value counts the stored bodies in sequence that came back to a request with `If-None-Match`. */
139
+ readonly etagMisses: number;
140
+ /** This value counts the stored bodies in sequence that came back to a request with only `If-Modified-Since`. */
141
+ readonly dateMisses: number;
142
+ /** This value counts the requests with only `If-Modified-Since` since the crawler stopped the ETag, or since the last probe. */
143
+ readonly sinceProbe: number;
144
+ /** This time (milliseconds since the epoch) is the last time that the crawler stopped or started the ETag, or probed it. */
145
+ readonly changedAt: number;
146
+ }
147
+ /** These are the limits of the validator memo. */
148
+ export interface ValidatorLimits {
149
+ /** After this number of stored bodies in sequence, a validator fails. The value is 3. */
150
+ readonly misses: number;
151
+ /** After this number of requests with only `If-Modified-Since`, the next request sends the ETag again (a probe). The value is 20. */
152
+ readonly probeEvery: number;
153
+ /** After this time with no probe, the next request is a probe. The value is 7 days. */
154
+ readonly probeAgeMs: number;
155
+ }
156
+ /** These are the limits of the validator memo of the crawler. */
157
+ export declare const VALIDATOR_MEMO: ValidatorLimits;
158
+ /**
159
+ * This function tells which validators the next conditional request to a host sends. It sends
160
+ * each validator of the stored copy, except when the memo stopped the ETag and the copy has a
161
+ * `Last-Modified` date. Then it sends only `If-Modified-Since`, and the ETag too for a probe.
162
+ * A probe comes after `probeEvery` requests or after `probeAgeMs`.
163
+ */
164
+ export declare const validatorsToSend: (memo: ValidatorMemo | undefined, stored: {
165
+ readonly etag: boolean;
166
+ readonly lastModified: boolean;
167
+ }, now: number) => {
168
+ readonly etag: boolean;
169
+ readonly lastModified: boolean;
170
+ };
171
+ /**
172
+ * This function adds one conditional request to the memo of a host, and gives the new memo.
173
+ * These are the rules, with the limits of `VALIDATOR_MEMO`:
174
+ *
175
+ * 1. A 304 to a request with `If-None-Match` marks the ETag `works`. After a probe, the crawler sends the ETag again.
176
+ * 2. After `misses` stored bodies in sequence with `If-None-Match`, the ETag fails. When the request also sent
177
+ * `If-Modified-Since`, the crawler stops the ETag for the host.
178
+ * 3. A probe that gets a 200 keeps the ETag stopped, and the count to the next probe starts again.
179
+ * 4. After `misses` stored bodies in sequence with only `If-Modified-Since`, that validator fails too. Then the
180
+ * crawler sends the two validators again, because they cost nothing. It tries the date alone again after `probeAgeMs`.
181
+ * 5. A changed body tells nothing about a validator.
182
+ */
183
+ export declare const nextValidatorMemo: (memo: ValidatorMemo | undefined, report: ValidatorReport, now: number) => ValidatorMemo;
184
+ /**
185
+ * This store keeps the state of each host in memory, for one process. It removes the state of an
186
+ * idle host (no rest left, no lease, no counts and no stop) when it holds more than `maxHosts` hosts.
187
+ * It keeps the validator memos of the `maxHosts` hosts that it used last.
188
+ */
189
+ export declare class MemoryHostStateStore implements HostStateStore {
190
+ private readonly maxHosts;
191
+ private readonly states;
192
+ /** The validator memos, the memo used last at the end. */
193
+ private readonly memos;
194
+ private sweepAt;
195
+ constructor(maxHosts?: number);
196
+ /** This value is the number of hosts that the store holds. */
197
+ get size(): number;
198
+ private state;
199
+ private sweep;
200
+ reserve(host: string, now: number, leaseMs: number): number;
201
+ record(host: string, result: HostResult): HostCounts;
202
+ release(host: string, nextAt: number): void;
203
+ defer(host: string, until: number): void;
204
+ stop(host: string, until: number, reason: string): void;
205
+ stopped(host: string, now: number): HostStop | undefined;
206
+ reset(host?: string): void;
207
+ validators(host: string): ValidatorMemo | undefined;
208
+ noteValidators(host: string, report: ValidatorReport, now: number): ValidatorMemo;
209
+ }
210
+ /** These options change the scheduler. */
211
+ export interface SchedulerOptions {
212
+ /** This store keeps the state of each host. The default is a `MemoryHostStateStore`. */
213
+ readonly store?: HostStateStore | undefined;
214
+ /** This value is the longest slot of one request. A slot that a crashed process holds ends after it. The default is 5 minutes. */
215
+ readonly leaseMs?: number | undefined;
216
+ /** A host rests this long after a task that throws an error, or a task that gives no valid delay. The default is 1 second. */
217
+ readonly fallbackDelayMs?: number | undefined;
218
+ }
219
+ export declare class HostScheduler {
220
+ private readonly clock;
221
+ private readonly maxActive;
222
+ /** This store keeps the state of each host. */
223
+ readonly store: HostStateStore;
224
+ private readonly chains;
225
+ private active;
226
+ private readonly queue;
227
+ private readonly leaseMs;
228
+ private readonly fallbackDelayMs;
229
+ constructor(clock: Clock, maxActive: number, options?: SchedulerOptions);
230
+ private acquire;
231
+ private release;
232
+ private sleep;
233
+ /**
234
+ * This method starts one request on a host, in sequence, after the rest of the host. When the
235
+ * signal aborts during the wait, the promise rejects with the reason of the signal.
236
+ */
237
+ run<T>(host: string, task: () => Promise<Paced<T>>, signal?: AbortSignal): Promise<T>;
238
+ /** The server asked for a pause (Retry-After), or a backoff chose one. */
239
+ park(host: string, untilMs: number): void;
240
+ /** This method stops a host until `untilMs`. With no time, the stop has no end. */
241
+ stop(host: string, reason: string, untilMs?: number): void;
242
+ stoppedReason(host: string): string | undefined;
243
+ /** This method gives the stop of a host, with its end, when the host is stopped. */
244
+ stopOf(host: string): HostStop | undefined;
245
+ /** This value counts the failures of a host in sequence, for the backoff. A success sets it to zero. */
246
+ failed(host: string): number;
247
+ /** This value counts the requests to wait (429, 503) of a host in sequence. They do not stop the host. */
248
+ waited(host: string): number;
249
+ succeeded(host: string): void;
250
+ /** This method removes the stops and the counts of one host, or of each host. The pace stays. */
251
+ reset(host?: string): void;
252
+ }
253
+ /**
254
+ * This function gives an exponential backoff with full jitter, from the analysis of AWS. The wait
255
+ * is random, up to base × 2^(try − 1), with a maximum. The jitter keeps retries apart.
256
+ */
257
+ export declare const backoffMs: (attempt: number, baseMs: number, capMs: number, random: () => number) => number;
258
+ /**
259
+ * This function gives an exponential backoff with equal jitter. The wait is half of
260
+ * base × 2^(try − 1), with a maximum, and a random part up to the other half. Thus each backoff
261
+ * is at least half of its step, and it is not lost under the rest of the host.
262
+ */
263
+ export declare const equalJitterMs: (attempt: number, baseMs: number, capMs: number, random: () => number) => number;
264
+ /** These are the facts of one request that set the rest of its host. */
265
+ export interface RestInput {
266
+ /** This value is the kind of the request: a web page, a documented API, or a render in the browser. */
267
+ readonly kind: "page" | "api" | "render";
268
+ /** This value is the duration of the request, in milliseconds. */
269
+ readonly elapsedMs: number;
270
+ /** This value is the `crawl-delay` of robots.txt, in seconds. */
271
+ readonly crawlDelaySeconds?: number | undefined;
272
+ /** This value is the documented pace of the source. It replaces the interval of the policy for the kind. */
273
+ readonly minIntervalMs?: number | undefined;
274
+ /** This value is the wait that the server asked for (`Retry-After`). */
275
+ readonly retryAfterMs?: number | undefined;
276
+ }
277
+ /** The rest of a host after a request, and the reason for its length. */
278
+ export interface Rest {
279
+ readonly ms: number;
280
+ readonly reason: "pace" | "slow" | "retry-after" | "crawl-delay";
281
+ }
282
+ /**
283
+ * This function gives the rest of a host after one request, as the crawler calculates it. These
284
+ * are the parts:
285
+ *
286
+ * - The interval of the kind (3 seconds for a page), or the documented pace of the source.
287
+ * - The `crawl-delay` of robots.txt, up to `crawlDelayCapMs`, when it is longer.
288
+ * - After a page or a render, `elapsedFactor` times the duration of the request, up to `elapsedCapMs`.
289
+ * - The `Retry-After` of the server, when it is longer.
290
+ *
291
+ * The backoff after an error or a request to wait comes on top, with jitter. The function is
292
+ * pure, so a simulation and a study can use the pace of the crawler.
293
+ */
294
+ export declare const restAfter: (policy: PolitenessPolicy, input: RestInput) => Rest;
295
+ /**
296
+ * The politeness policy holds each limit of the crawler. Each value has a default, and a host can
297
+ * have values of its own. The times are in milliseconds.
298
+ */
299
+ export interface PolitenessPolicy {
300
+ /** This value is the shortest rest of a host between two page requests. The default is 3 seconds. */
301
+ readonly pageIntervalMs: number;
302
+ /** This value is the shortest rest of a host between two API requests. The default is 1 second. */
303
+ readonly apiIntervalMs: number;
304
+ /** This value is the shortest rest of a host after a render. The default is 5 seconds. */
305
+ readonly renderIntervalMs: number;
306
+ /** After a page, the host rests this number of times the duration of the request. The default is 5. */
307
+ readonly elapsedFactor: number;
308
+ /** This value is the maximum of the rest that comes from the duration. The default is 30 seconds. */
309
+ readonly elapsedCapMs: number;
310
+ /** This value is the maximum `crawl-delay` that the crawler obeys. The default is 60 seconds. */
311
+ readonly crawlDelayCapMs: number;
312
+ /** This value is the maximum number of redirects for one fetch. The default is 5. */
313
+ readonly maxRedirects: number;
314
+ /** This value is the maximum size of a body after decompression. The default is 5 MiB. */
315
+ readonly maxBytes: number;
316
+ /** This value is the deadline of one request, the body included. The default is 90 seconds. */
317
+ readonly requestTimeoutMs: number;
318
+ /** This value is the deadline of one render. The default is 20 seconds. */
319
+ readonly renderTimeoutMs: number;
320
+ /** After this number of failures in sequence, the host stops. The default is 5. */
321
+ readonly maxFailures: number;
322
+ /** This value is the stop after too many failures. The default is 1 hour. */
323
+ readonly failureStopMs: number;
324
+ /** This value is the stop after a 401 or a 403. The default is 1 hour. */
325
+ readonly blockedStopMs: number;
326
+ /** This value is the stop after a bot challenge. The default is 24 hours. */
327
+ readonly challengeStopMs: number;
328
+ /** This value is the stop after a 402. The default is 24 hours. */
329
+ readonly paymentStopMs: number;
330
+ /** The crawler obeys a `Retry-After` up to this value. A longer wait stops the host for that time. The default is 1 hour. */
331
+ readonly maxRetryAfterMs: number;
332
+ /** This value is the first step of the backoff after an error or a request to wait. The default is 5 seconds. */
333
+ readonly backoffBaseMs: number;
334
+ /** This value is the maximum step of the backoff. The default is 300 seconds. */
335
+ readonly backoffCapMs: number;
336
+ /** The crawler keeps a robots.txt for this time. The default is 24 hours. */
337
+ readonly robotsTtlMs: number;
338
+ /** The crawler keeps an unreachable robots.txt verdict for this time first, with a backoff. The default is 5 minutes. */
339
+ readonly robotsRetryMs: number;
340
+ /** This value is the maximum time for an unreachable robots.txt verdict. The default is 1 hour. */
341
+ readonly robotsRetryCapMs: number;
342
+ /** A stored robots.txt younger than this age replaces a robots.txt that the crawler cannot read. The default is 30 days. */
343
+ readonly robotsStaleMs: number;
344
+ /** This value is the maximum freshness that the crawler gives a response with no freshness of its own. The default is 24 hours. */
345
+ readonly heuristicFreshnessCapMs: number;
346
+ }
347
+ /**
348
+ * This value is the longest stop that a `Retry-After` can give: 30 days. A wait longer than
349
+ * `maxRetryAfterMs` stops the host for the time that the server asks, up to this value.
350
+ */
351
+ export declare const MAX_STOP_MS: number;
352
+ export declare const DEFAULT_POLICY: PolitenessPolicy;
353
+ /**
354
+ * These options change the politeness policy. The values at the top apply to each host. The
355
+ * `hosts` map gives values for one host (`api.example.org`) or for the hosts under a name
356
+ * (`*.example.org`). The most specific entry wins.
357
+ */
358
+ export interface PolicyOptions extends Partial<PolitenessPolicy> {
359
+ readonly hosts?: Readonly<Record<string, Partial<PolitenessPolicy>>> | undefined;
360
+ }
361
+ /** This function examines the options of a policy. It throws a `RangeError` for a value that is not a valid number. */
362
+ export declare const checkPolicy: (options: PolicyOptions | undefined) => void;
363
+ /** This function gives the policy of one host: the defaults, then the general options, then the entries for the host. */
364
+ export declare const policyFor: (options: PolicyOptions | undefined, host: string) => PolitenessPolicy;
365
+ //#endregion
366
+ //# sourceMappingURL=politeness.d.mts.map
@@ -1,5 +1,5 @@
1
- import { i as durationMs } from "./version-BPem-XZd.mjs";
2
- import { C as fallbackReason, S as archivedOf, c as isHeldBack, i as fetchAndIndex, n as HELD_BACK, p as recordOverride, s as heldBackMeta, y as archivedCopy } from "./runtime-D_r2SYge.mjs";
1
+ import { i as durationMs } from "./version-Djva7JkN.mjs";
2
+ import { b as fallbackReason, c as isHeldBack, d as recordOverride, g as archivedCopy, i as fetchAndIndex, n as HELD_BACK, s as heldBackMeta, y as archivedOf } from "./runtime-C_2BQibH.mjs";
3
3
  import { o as decodeBody } from "./html-DkQUAE_d.mjs";
4
4
  //#region src/recipe.ts
5
5
  /**
@@ -27,7 +27,11 @@ const RESERVED_RECIPES = [
27
27
  "sitemap",
28
28
  "feed"
29
29
  ];
30
- /** These are the limits of a recipe. */
30
+ /**
31
+ * These are the limits of a recipe.
32
+ *
33
+ * @beta
34
+ */
31
35
  const RECIPE_LIMITS = {
32
36
  nameChars: 64,
33
37
  docIdChars: 2048
@@ -144,6 +148,8 @@ const defineRecipe = (definition) => {
144
148
  * This function gives the extractor of a recipe to the extractors of a runtime. A second
145
149
  * registration of the same recipe does nothing. A different extractor with the same name throws
146
150
  * an error, because `reindex` must read each capture with one extractor.
151
+ *
152
+ * @beta
147
153
  */
148
154
  const registerRecipe = (extractors, recipe) => {
149
155
  const known = extractors.get(recipe.name);
@@ -276,4 +282,4 @@ const runRecipe = async (target, recipe, input, options = {}) => {
276
282
  //#endregion
277
283
  export { readJson as a, runtimeOf as c, defineRecipe as i, RESERVED_RECIPES as n, registerRecipe as o, deadlineOf as r, runRecipe as s, RECIPE_LIMITS as t };
278
284
 
279
- //# sourceMappingURL=recipe-GiFQojmo.mjs.map
285
+ //# sourceMappingURL=recipe-B2SkepDZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipe-B2SkepDZ.mjs","names":[],"sources":["../src/recipe.ts"],"sourcesContent":["/*\n * The recipe SDK. A recipe tells the crawler what to fetch and how to read it into documents.\n *\n * - `defineRecipe` declares a recipe: its name, the request for an input, and its extractor.\n * - `runRecipe` fetches one input through the crawler and gives the documents of the capture.\n * - `registerRecipe` gives the extractor to the extractors of a runtime. `openCrawler` and\n * `createRuntime` take `recipes` and register them at open, so `reindex` reads their captures.\n *\n * Each fetch of a recipe is a fetch of the crawler. Thus robots.txt (for a page), the pace of each\n * host, the stops, the egress policy and the capture log apply. The documents are versions in the\n * index, so `find`, `read`, `verify`, `extract`, `diff` and the time travel use them.\n */\nimport type { FetchRequest } from \"./crawler.ts\";\nimport type { Doc, ExtractedDoc, VersionText } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedCopy, archivedOf, fallbackReason } from \"./fallback.ts\";\nimport type { CrawlRuntime, Extractor, Extractors, Indexed, PageOptions } from \"./runtime.ts\";\nimport { HELD_BACK, fetchAndIndex, heldBackMeta, isHeldBack, recordOverride } from \"./runtime.ts\";\nimport type { AiInput } from \"./signals.ts\";\nimport { decodeBody } from \"./text.ts\";\n\n/** The capture that an extractor reads: the final URL of the response and its content type. */\nexport interface RecipeCapture {\n readonly url: string;\n readonly contentType: string | null;\n}\n\n/**\n * This function reads one captured body into the documents that it holds. It is pure: the same\n * body and capture always give the same documents. It sends no request. It can answer later.\n */\nexport type RecipeExtract = (\n body: Uint8Array,\n capture: RecipeCapture,\n) => ReadonlyArray<ExtractedDoc> | Promise<ReadonlyArray<ExtractedDoc>>;\n\n/** The request of a recipe for one input: the URL, and the parts that change with the input. */\nexport interface RecipeRequest {\n readonly url: string;\n /** This value replaces the `accept` of the recipe for this request. */\n readonly accept?: string | undefined;\n /** These headers go with the request. The crawler does not write them into the capture. */\n readonly headers?: Readonly<Record<string, string>> | undefined;\n /** These query parameters hold secrets. The capture shows `REDACTED` in their place. */\n readonly secretParams?: readonly string[] | undefined;\n}\n\n/** These values declare a recipe. Only `name` and `extract` are necessary. */\nexport interface RecipeDefinition<Input = string> {\n /**\n * This name goes into each capture record of the recipe, so it is part of each capture id.\n * `reindex` finds the extractor by it. Do not change it after the first capture.\n */\n readonly name: string;\n /** This text tells what the recipe reads. */\n readonly description?: string | undefined;\n /**\n * The value `page` (the default) is a web page: robots.txt applies. The value `api` is a\n * documented API with terms of its own: robots.txt does not apply, and the pace still does.\n */\n readonly mode?: \"api\" | \"page\" | undefined;\n /** This value is the `Accept` header. The defaults are HTML for a page and JSON for an API. */\n readonly accept?: string | undefined;\n /** This value is the documented pace of the source: the shortest time between two of its requests. */\n readonly minInterval?: Duration | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** This value is the most bytes of a body that the crawler keeps. */\n readonly maxBytes?: number | undefined;\n /** This function gives the request for an input. With no function, the input is the URL. */\n readonly fetch?: ((input: Input) => string | RecipeRequest) | undefined;\n /** This function reads a captured body into documents. It is pure, and it sends no request. */\n readonly extract: RecipeExtract;\n}\n\n/** These options build the request of a recipe for one input (`Recipe.request`). */\nexport interface RecipeRequestOptions {\n /** This value replaces the `maxAge` of the recipe. The value `0` asks the source again. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for a recipe of the mode `page`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n /** This name connects the captures to the job that asked for them. */\n readonly job?: string | undefined;\n /** When this signal aborts, the crawler stops the fetch. */\n readonly signal?: AbortSignal | undefined;\n /** This label goes into each capture record of the request. */\n readonly scope?: string | undefined;\n /** These labels go into each capture record of the request. They are not part of the capture id. */\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** No request starts at or after this time (milliseconds since the epoch, on the clock of the crawler). */\n readonly deadline?: number | undefined;\n}\n\n/** A recipe that `defineRecipe` made. Give it to `runRecipe`, to `registerRecipe` or to `openCrawler`. */\nexport interface Recipe<Input = string> {\n readonly name: string;\n readonly description: string | null;\n readonly mode: \"api\" | \"page\";\n readonly accept: string;\n /**\n * This extractor reads the captures of the recipe. It checks the documents of the definition.\n *\n * @beta\n */\n readonly extractor: Extractor;\n /**\n * This method gives the request of the crawler for one input. A value that is not a URL throws a `TypeError`.\n *\n * @beta\n */\n request(input: Input, options?: RecipeRequestOptions): FetchRequest;\n}\n\n/**\n * This function reads a body as JSON, in the encoding that the response declares. A body that is\n * not JSON (a page about maintenance, or a login page) throws an error that names the URL and the\n * content type. In an extractor, the runtime then reports that error.\n */\nexport const readJson = (body: Uint8Array, capture: RecipeCapture): unknown => {\n const text = decodeBody(body, capture.contentType);\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new Error(\n `${capture.url} answered ${capture.contentType ?? \"a body of no stated type\"}, not JSON`,\n );\n }\n};\n\n/**\n * These recipe names belong to the crawler: the page reader, the archived fallback, robots.txt,\n * and the sitemaps and feeds of `discover`. A recipe cannot use them.\n */\nexport const RESERVED_RECIPES: readonly string[] = [\n \"page\",\n \"archive-copy\",\n \"archive-index\",\n \"robots.txt\",\n \"sitemap\",\n \"feed\",\n];\n\n/**\n * These are the limits of a recipe.\n *\n * @beta\n */\nexport const RECIPE_LIMITS: {\n /** The longest recipe name. */\n readonly nameChars: number;\n /** The longest document id. */\n readonly docIdChars: number;\n} = { nameChars: 64, docIdChars: 2048 };\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;\nconst PAGE_ACCEPT = \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.5\";\nconst API_ACCEPT = \"application/json\";\nconst AI_INPUTS: readonly AiInput[] = [\"yes\", \"no\", \"unset\"];\n\n/** This function tells what is wrong with one document of an extractor, or gives undefined. */\nconst faultOf = (value: unknown, seen: Set<string>): string | undefined => {\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return \"a document must be an object\";\n const doc = value as Record<string, unknown>;\n const id = doc[\"doc\"];\n if (typeof id !== \"string\" || id.length === 0 || id.length > RECIPE_LIMITS.docIdChars)\n return `doc must be a text of 1 to ${String(RECIPE_LIMITS.docIdChars)} characters`;\n // the archived fallback labels the copies of third parties, and only it makes these\n if (id.startsWith(\"archive:\"))\n return `the id ${id} starts with archive:, which is for the archived copies`;\n if (seen.has(id)) return `two documents have the id ${id}`;\n seen.add(id);\n const kind = doc[\"kind\"];\n if (typeof kind !== \"string\" || kind.length === 0) return \"kind must be a text that is not empty\";\n if (kind === \"archived\") return \"the kind archived is for the archived copies\";\n if (typeof doc[\"title\"] !== \"string\") return \"title must be a text\";\n if (typeof doc[\"text\"] !== \"string\") return \"text must be a text\";\n if (doc[\"url\"] !== null && typeof doc[\"url\"] !== \"string\") return \"url must be a text or null\";\n const meta = doc[\"meta\"];\n if (meta === null || typeof meta !== \"object\" || Array.isArray(meta))\n return \"meta must be an object\";\n try {\n JSON.stringify(meta);\n } catch {\n return \"meta must be JSON: no cycle and no BigInt\";\n }\n const merge = doc[\"merge\"];\n if (merge !== undefined && merge !== \"latest\" && merge !== \"richest\")\n return 'merge must be \"latest\" or \"richest\"';\n if (doc[\"full_text\"] !== undefined && typeof doc[\"full_text\"] !== \"string\")\n return \"full_text must be a text\";\n const aiInput = doc[\"ai_input\"];\n if (aiInput !== undefined && !AI_INPUTS.includes(aiInput as AiInput))\n return 'ai_input must be \"yes\", \"no\" or \"unset\"';\n return undefined;\n};\n\n/** This function gives the documents of an extractor, or throws an error that names the first fault. */\nconst checkedDocs = (name: string, docs: unknown): ReadonlyArray<ExtractedDoc> => {\n if (!Array.isArray(docs))\n throw new TypeError(`the recipe ${name} gave ${typeof docs}, not an array of documents`);\n const seen = new Set<string>();\n for (const [index, doc] of docs.entries()) {\n const fault = faultOf(doc, seen);\n if (fault !== undefined)\n throw new TypeError(\n `the recipe ${name} gave a document that is not valid (${String(index)}): ${fault}`,\n );\n }\n return docs as ReadonlyArray<ExtractedDoc>;\n};\n\n/**\n * This function declares a recipe. It checks the definition at once: a name that is not valid, a\n * reserved name or a duration that it cannot read throws an error. The extractor of the recipe\n * checks each document of `extract`. A document that is not valid makes the extractor throw, and\n * the runtime reports it as an error of the extractor.\n */\nexport const defineRecipe = <Input = string>(\n definition: RecipeDefinition<Input>,\n): Recipe<Input> => {\n const name = definition.name;\n if (typeof name !== \"string\" || name.length > RECIPE_LIMITS.nameChars || !NAME.test(name)) {\n throw new RangeError(\n `a recipe name is 1 to ${String(RECIPE_LIMITS.nameChars)} letters, digits and . _ : -, and it starts with a letter or a digit, not ${JSON.stringify(name) ?? \"undefined\"}`,\n );\n }\n if (RESERVED_RECIPES.includes(name)) {\n throw new RangeError(\n `the recipe name ${name} belongs to the crawler: give the recipe a name of its own`,\n );\n }\n if (typeof definition.extract !== \"function\") {\n throw new TypeError(\n `the recipe ${name} needs extract, a function that reads a body into documents`,\n );\n }\n const mode = definition.mode ?? \"page\";\n if (mode !== \"page\" && mode !== \"api\") {\n throw new RangeError(\n `the mode of the recipe ${name} must be \"page\" or \"api\", not ${String(mode)}`,\n );\n }\n const accept = definition.accept ?? (mode === \"api\" ? API_ACCEPT : PAGE_ACCEPT);\n const minIntervalMs =\n definition.minInterval === undefined ? undefined : durationMs(definition.minInterval);\n const maxAgeOf = (value: Duration): number | \"auto\" =>\n value === \"auto\" ? \"auto\" : durationMs(value);\n const maxAge = maxAgeOf(definition.maxAge ?? \"24h\");\n const maxBytes = definition.maxBytes;\n if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 1)) {\n throw new RangeError(`maxBytes of the recipe ${name} must be a whole number of 1 or more`);\n }\n const extract = definition.extract;\n const extractor: Extractor = async (body, capture) =>\n checkedDocs(name, await extract(body, { url: capture.url, contentType: capture.contentType }));\n const fetch = definition.fetch;\n return {\n name,\n description: definition.description ?? null,\n mode,\n accept,\n extractor,\n request(input, options = {}) {\n const asked: unknown = fetch === undefined ? input : fetch(input);\n const target = typeof asked === \"string\" ? { url: asked } : (asked as RecipeRequest | null);\n const url = target?.url;\n let parsed: URL | undefined;\n try {\n parsed = typeof url === \"string\" ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\")) {\n throw new TypeError(\n `the recipe ${name} needs an http or https URL for its input, not ${JSON.stringify(url) ?? String(url)}`,\n );\n }\n return {\n url: parsed.href,\n mode,\n recipe: name,\n accept: target?.accept ?? accept,\n headers: target?.headers,\n secretParams: target?.secretParams,\n minIntervalMs,\n maxAgeMs: options.maxAge === undefined ? maxAge : maxAgeOf(options.maxAge),\n maxBytes,\n onBehalfOf: options.onBehalfOf,\n job: options.job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline: options.deadline,\n };\n },\n };\n};\n\n/**\n * This function gives the extractor of a recipe to the extractors of a runtime. A second\n * registration of the same recipe does nothing. A different extractor with the same name throws\n * an error, because `reindex` must read each capture with one extractor.\n *\n * @beta\n */\nexport const registerRecipe = <Input>(extractors: Extractors, recipe: Recipe<Input>): void => {\n const known = extractors.get(recipe.name);\n if (known === recipe.extractor) return;\n if (known !== undefined) {\n throw new Error(\n `the recipe ${recipe.name} has a different extractor in this runtime: give each recipe a name of its own`,\n );\n }\n extractors.register(recipe.name, recipe.extractor);\n};\n\n/** These options control one run of a recipe. */\nexport interface RecipeRunOptions extends Omit<RecipeRequestOptions, \"deadline\"> {\n /** No request starts after this time from the start of the run, for example `\"30s\"`. */\n readonly budget?: Duration | undefined;\n /** This value is true when a person gave this input directly. It is the only way to see a held-back document. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /**\n * The value `archive` gives a copy from a public web archive when the site refuses the page or is\n * down. Only a recipe of the mode `page` takes it. The page reader reads the copy.\n */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n /** The value true also gives an archived copy of a page that the current robots.txt disallows. */\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** One document of a run of a recipe: the version of the capture of the run. */\nexport interface RecipeDoc {\n readonly doc: string;\n readonly kind: string;\n /** This value is the title. A held-back document shows no title. */\n readonly title: string;\n readonly url: string | null;\n /** This value is the metadata. A held-back document shows only the structural fields. */\n readonly meta: Readonly<Record<string, unknown>>;\n /** This value is the length of the text. `read` gives the text. */\n readonly chars: number;\n /** This id is the capture of this version. A citation names it. */\n readonly capture: string;\n readonly fetched_at: string;\n readonly held_back: boolean;\n /** This value tells that the document is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n}\n\n/** The result of `runRecipe`. */\nexport interface RecipeResult {\n readonly recipe: string;\n /** This URL is the URL of the capture. Its secrets show as `REDACTED`. */\n readonly url: string;\n readonly outcome: string;\n readonly status: number;\n /** These documents came from the capture of the run, in the order of the extractor. */\n readonly docs: readonly RecipeDoc[];\n /** This id is the capture of the documents. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n /** This value is true when the stored copy answered, and the crawler sent no request. */\n readonly from_cache: boolean;\n /** This value is true when a document is held back. */\n readonly held_back: boolean;\n /** This text is the error of the extractor, when it threw for the capture. */\n readonly error: string | null;\n /** This value tells that the documents are an archived copy, not a fresh fetch. */\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\n/** A recipe operates on a runtime, or on a crawler that has one (`crawler.runtime`). */\nexport type RecipeTarget = CrawlRuntime | { readonly runtime: CrawlRuntime };\n\n/** This function gives the runtime of a target. */\nexport const runtimeOf = (target: RecipeTarget): CrawlRuntime =>\n \"crawler\" in target ? target : target.runtime;\n\n/** This function gives the deadline of a budget, on the clock of the runtime. */\nexport const deadlineOf = (\n runtime: CrawlRuntime,\n budget: Duration | undefined,\n): number | undefined =>\n budget === undefined ? undefined : Date.parse(runtime.now()) + durationMs(budget);\n\n/** This function gives one document of a run, with the rule for a held-back document. */\nconst recipeDoc = async (\n runtime: CrawlRuntime,\n doc: Doc,\n capture: string | undefined,\n userProvided: boolean,\n action: string,\n): Promise<RecipeDoc> => {\n const current = runtime.index.get(doc.doc) ?? doc;\n const version: VersionText | Doc =\n (capture === undefined ? undefined : runtime.index.versionText(doc.doc, capture)) ?? current;\n const aiInput: AiInput =\n current.ai_input === \"no\" || version.ai_input === \"no\" ? \"no\" : version.ai_input;\n const held = isHeldBack({ ai_input: aiInput }, userProvided);\n if (aiInput === \"no\" && userProvided) await recordOverride(runtime, current, action);\n const archived = archivedOf(version.meta);\n return {\n doc: doc.doc,\n kind: version.kind,\n title: held ? \"\" : version.title,\n url: version.url,\n meta: held ? heldBackMeta(version.meta) : version.meta,\n chars: version.text.length,\n capture: version.capture,\n fetched_at: version.fetched_at,\n held_back: held,\n ...(archived === undefined ? {} : { archived }),\n };\n};\n\nconst noteOf = (parts: ReadonlyArray<string | null | undefined>): string | null => {\n const kept = parts.filter((part): part is string => typeof part === \"string\" && part.length > 0);\n return kept.length > 0 ? kept.join(\"; \") : null;\n};\n\n/**\n * This function starts a recipe for one input. It fetches the request through the crawler, and it\n * reads the capture with the extractor of the recipe. It gives the documents of that capture.\n * These are its rules:\n *\n * - The runtime registers the recipe the first time that it gets it. Give the recipe to\n * `openCrawler` too, so that `reindex` reads its captures before the first fetch.\n * - A stored copy younger than `maxAge` answers with no request. When the extractor cannot read a\n * stored copy, the crawler asks the source one more time.\n * - An extractor that throws gives no document, and `error` tells why. The fetch still succeeds.\n * - A document that asked not to be AI input shows no title and only its structural metadata,\n * unless `userProvided`. The crawler logs each `userProvided` read of a held-back document.\n * - With `fallback: \"archive\"`, a page that the site refuses gets an archived copy, with\n * `archived`. The page reader reads the copy. The extractor of the recipe reads only live\n * captures. Thus its documents do not mix a live answer and an old copy of a third party.\n */\nexport const runRecipe = async <Input>(\n target: RecipeTarget,\n recipe: Recipe<Input>,\n input: Input,\n options: RecipeRunOptions = {},\n): Promise<RecipeResult> => {\n const runtime = runtimeOf(target);\n if (options.fallback === \"archive\" && recipe.mode !== \"page\") {\n throw new RangeError(\n `the recipe ${recipe.name} reads an API: an archived copy is for a page, so give fallback only to a recipe of the mode page`,\n );\n }\n registerRecipe(runtime.extractors, recipe);\n const deadline = deadlineOf(runtime, options.budget);\n const request = recipe.request(input, { ...options, deadline });\n let indexed: Indexed = await fetchAndIndex(runtime, request);\n // a stored answer that the extractor cannot read is no answer: it would give the same error until maxAge\n if (indexed.error !== undefined && indexed.blocked === undefined && indexed.fetched.fromCache) {\n indexed = await fetchAndIndex(runtime, { ...request, maxAgeMs: 0 });\n }\n const userProvided = options.userProvided ?? false;\n const action = options.job ?? `recipe ${recipe.name}`;\n const fetched = indexed.fetched;\n const notes: Array<string | undefined> = [fetched.note, indexed.error];\n if (indexed.docs.length === 0 && options.fallback === \"archive\") {\n const reason = fallbackReason(indexed);\n if (reason !== undefined) {\n const page: PageOptions = {\n render: \"never\",\n maxAgeMs: request.maxAgeMs,\n onBehalfOf: request.onBehalfOf,\n job: request.job,\n signal: request.signal,\n scope: request.scope,\n labels: request.labels,\n deadline,\n fallback: \"archive\",\n archiveDespiteRobots: options.archiveDespiteRobots,\n };\n const answer = await archivedCopy(runtime, request.url, reason, page);\n if (answer.doc !== undefined && answer.archived !== undefined) {\n const doc = await recipeDoc(runtime, answer.doc, answer.doc.capture, userProvided, action);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs: [doc],\n capture: doc.capture,\n fetched_at: doc.fetched_at,\n from_cache: answer.fromCache,\n held_back: doc.held_back,\n error: indexed.error ?? null,\n archived: answer.archived,\n note: noteOf([\n ...notes,\n ...answer.notes,\n \"the page reader read the archived copy, not the extractor of the recipe\",\n doc.held_back ? HELD_BACK : undefined,\n ]),\n };\n }\n notes.push(...answer.notes);\n }\n }\n const capture = fetched.capture;\n const docs: RecipeDoc[] = [];\n for (const doc of indexed.docs)\n docs.push(await recipeDoc(runtime, doc, capture, userProvided, action));\n const held = docs.some((doc) => doc.held_back);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs,\n capture: docs[0]?.capture ?? capture ?? null,\n fetched_at: docs[0]?.fetched_at ?? fetched.fetchedAt ?? null,\n from_cache: fetched.fromCache,\n held_back: held,\n error: indexed.error ?? null,\n note: noteOf([...notes, held ? HELD_BACK : undefined]),\n };\n};\n"],"mappings":"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"}
@@ -1,10 +1,14 @@
1
- import { i as durationMs } from "./version-BPem-XZd.mjs";
2
- import { S as archivedOf, a as fetchPage, c as isHeldBack, n as HELD_BACK, p as recordOverride } from "./runtime-D_r2SYge.mjs";
1
+ import { i as durationMs } from "./version-Djva7JkN.mjs";
2
+ import { a as fetchPage, c as isHeldBack, d as recordOverride, n as HELD_BACK, y as archivedOf } from "./runtime-C_2BQibH.mjs";
3
3
  import { o as decodeBody, r as typesOf, t as extractPage } from "./html-DkQUAE_d.mjs";
4
- import { c as runtimeOf, r as deadlineOf } from "./recipe-GiFQojmo.mjs";
4
+ import { c as runtimeOf, r as deadlineOf } from "./recipe-B2SkepDZ.mjs";
5
5
  import { DAYS, describeWeek, disagreements, fromCompact, fromSchemaOrg, fromText } from "./hours.mjs";
6
6
  //#region src/recipe-venue.ts
7
- /** These are the limits of the venue recipe. */
7
+ /**
8
+ * These are the limits of the venue recipe.
9
+ *
10
+ * @beta
11
+ */
8
12
  const VENUE_LIMITS = {
9
13
  pages: 3,
10
14
  maxPages: 8,
@@ -27,6 +31,8 @@ const wordsOf = (text) => text.toLowerCase().normalize("NFKD").replace(/\p{M}/gu
27
31
  * This function tells if a page names a venue: the page has the first word of the name and 60
28
32
  * percent of its words. A domain that a venue let go can show a different business. It gives
29
33
  * undefined for a name with no word.
34
+ *
35
+ * @beta
30
36
  */
31
37
  const namesVenue = (name, page) => {
32
38
  const words = wordsOf(name);
@@ -60,6 +66,8 @@ const withoutHash = (url) => url.replace(/#.*$/s, "").replace(/\/$/, "");
60
66
  * This function ranks the links of a page by the words of their text and path. The links to
61
67
  * the hours or a menu come first. It keeps only the links on the site of `home`,
62
68
  * and it leaves out the files (a PDF, an image) and the home page itself.
69
+ *
70
+ * @beta
63
71
  */
64
72
  const rankHoursLinks = (links, home) => {
65
73
  const site = hostOf(home);
@@ -164,6 +172,8 @@ const addressText = (value) => {
164
172
  /**
165
173
  * This function tells if a JSON-LD location record is the venue. The record must have the same
166
174
  * number on a street that shares a word, or a position near the venue (`VENUE_LIMITS.nearMetres`).
175
+ *
176
+ * @beta
167
177
  */
168
178
  const locationMatches = (record, location) => {
169
179
  const geo = record["geo"];
@@ -450,7 +460,11 @@ const readVenue = async (target, url, options = {}) => {
450
460
  };
451
461
  //#endregion
452
462
  //#region src/recipe-product.ts
453
- /** These are the limits of the product reader. A page with more is hostile or broken, and the reader stops there. */
463
+ /**
464
+ * These are the limits of the product reader. A page with more is hostile or broken, and the reader stops there.
465
+ *
466
+ * @beta
467
+ */
454
468
  const PRODUCT_LIMITS = {
455
469
  records: 500,
456
470
  offers: 200,
@@ -502,6 +516,8 @@ const fractionDigits = (currency) => {
502
516
  * separator, each followed by three digits, separates thousands: `1.299` is 1299. For a
503
517
  * currency with three decimal digits, one point with three digits is the decimal point. A text
504
518
  * with two numbers (a range), a negative amount or zero gives undefined.
519
+ *
520
+ * @beta
505
521
  */
506
522
  const readAmount = (value, currency) => {
507
523
  if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : void 0;
@@ -619,6 +635,8 @@ const sizeOf = (record) => {
619
635
  * 3. Else the largest product.
620
636
  *
621
637
  * It also reads the `mainEntity` of a record (a `WebPage` or an `ItemPage` that names its product).
638
+ *
639
+ * @beta
622
640
  */
623
641
  const pickProduct = (records, page) => {
624
642
  let steps = 0;
@@ -654,7 +672,11 @@ const pickProduct = (records, page) => {
654
672
  steps
655
673
  };
656
674
  };
657
- /** This function gives the variant that a URL names: the `variant` parameter of Shopify, or a `sku` or `gtin` parameter. */
675
+ /**
676
+ * This function gives the variant that a URL names: the `variant` parameter of Shopify, or a `sku` or `gtin` parameter.
677
+ *
678
+ * @beta
679
+ */
658
680
  const pickVariant = (product, page) => {
659
681
  let params;
660
682
  try {
@@ -889,4 +911,4 @@ const readProduct = async (target, url, options = {}) => {
889
911
  //#endregion
890
912
  export { readAmount as a, locationMatches as c, readVenue as d, venueReadingsOf as f, productOf as i, namesVenue as l, pickProduct as n, readProduct as o, pickVariant as r, VENUE_LIMITS as s, PRODUCT_LIMITS as t, rankHoursLinks as u };
891
913
 
892
- //# sourceMappingURL=recipe-product-Dellqudu.mjs.map
914
+ //# sourceMappingURL=recipe-product-CyZE3Ukd.mjs.map