@yedoma-labs/ilyn-pages 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,19 @@
1
1
  # @yedoma-labs/ilyn-pages
2
2
 
3
- Async iteration over every page of a paginated HTTP API, with guards against the usual failure modes:
4
- endless cursor loops, skipped or duplicated rows, rate limits in the middle of a run and hanging requests.
3
+ <picture>
4
+ <source media="(max-width: 640px)" srcset="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized-mobile.png">
5
+ <img src="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized.png" alt="Project Header">
6
+ </picture>
7
+
8
+ [![CI](https://github.com/yedoma-labs/ilyn-pages/actions/workflows/ci.yml/badge.svg)](https://github.com/yedoma-labs/ilyn-pages/actions/workflows/ci.yml)
9
+ [![npm version](https://img.shields.io/npm/v/@yedoma-labs/ilyn-pages.svg)](https://www.npmjs.com/package/@yedoma-labs/ilyn-pages)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6-blue.svg)](https://www.typescriptlang.org/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+
13
+ > **ilyn** (*ilyn*) — Yakutian (Sakha) for "(noch zu bestätigen)"
14
+
15
+ Zero-dependency async iteration over every page of a paginated HTTP API, with guards against endless
16
+ cursor loops, skipped or duplicated rows, rate limits in the middle of a run and hanging requests.
5
17
 
6
18
  Zero runtime dependencies. Uses only `fetch`, `URL`, `AbortController` and `Headers`, so it runs on
7
19
  Node.js 20+, Bun and Deno.
@@ -22,13 +34,13 @@ Fetching "all pages" of an API looks trivial and fails in production:
22
34
 
23
35
  `ilyn-pages` is the generic layer for the rest.
24
36
 
25
- ## Install
37
+ ## Installation
26
38
 
27
39
  ```sh
28
40
  pnpm add @yedoma-labs/ilyn-pages
29
41
  ```
30
42
 
31
- ## Quickstart
43
+ ## Quick start
32
44
 
33
45
  ### Next link in the `Link` header
34
46
 
@@ -130,9 +142,13 @@ Items that were yielded before an error are not rolled back. Keep what you need
130
142
  | Foreign next URL | Next URL has another origin than the start URL | `INVALID_LINK` (unless `allowCrossOriginNext: true`) |
131
143
  | Broken `Link` header | Malformed link that mentions `rel="next"` and no valid next link | `INVALID_LINK` |
132
144
 
133
- Every error is a `PaginationError` with a `code` and a `page` number (the page whose request or
145
+ Guard errors are `PaginationError` instances with a `code` and a `page` number (the page whose request or
134
146
  response caused the error). Error messages never contain query values or cursors.
135
147
 
148
+ Other errors pass through unchanged: a non-2xx status from `createFetchPager` is a plain `Error`
149
+ (`Request failed with HTTP 500`), a body that is not JSON is a plain `Error` that names the status,
150
+ and errors from the injected `fetch` are rethrown as they are.
151
+
136
152
  ## API
137
153
 
138
154
  ### `paginate<T>(options): AsyncGenerator<T>`
package/dist/index.cjs CHANGED
@@ -255,7 +255,13 @@ function createFetchPager(options) {
255
255
  continue;
256
256
  }
257
257
  if (!res.ok) throw new Error(`Request failed with HTTP ${res.status}`);
258
- const body = await res.json();
258
+ let body;
259
+ try {
260
+ body = await res.json();
261
+ } catch (error) {
262
+ if (error instanceof SyntaxError) throw new Error(`Response of HTTP ${res.status} is not valid JSON`, { cause: error });
263
+ throw error;
264
+ }
259
265
  const items = extract(body);
260
266
  if (!Array.isArray(items)) throw new TypeError("extract() must return an array of items");
261
267
  const raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/link-header.ts","../src/mask-url.ts","../src/fetch-pager.ts","../src/paginate.ts"],"sourcesContent":["export type PaginationErrorCode =\n\t| \"CURSOR_LOOP\"\n\t| \"PAGE_LIMIT\"\n\t| \"RATE_LIMITED\"\n\t| \"TIMEOUT\"\n\t| \"ABORTED\"\n\t| \"EMPTY_PAGES_LOOP\"\n\t| \"INVALID_LINK\";\n\n/**\n * Error raised by the pagination guards.\n *\n * `page` is the 1-based number of the page whose request or response caused the error.\n * Adapters such as `createFetchPager` do not know the page number and use `0`; `paginate()`\n * replaces it with the real number before the error reaches the caller.\n */\nexport class PaginationError extends Error {\n\treadonly code: PaginationErrorCode;\n\treadonly page: number;\n\n\tconstructor(\n\t\tcode: PaginationErrorCode,\n\t\tpage: number,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.name = \"PaginationError\";\n\t\tthis.code = code;\n\t\tthis.page = page;\n\t}\n}\n","import { PaginationError } from \"./errors.js\";\n\ninterface LinkScan {\n\tlinks: Record<string, string>;\n\t/** True when a malformed link-value mentions `rel=\"next\"`. */\n\tmalformedNext: boolean;\n}\n\nconst TOKEN_CHAR = /[!#$%&'*+\\-.^_`|~0-9A-Za-z]/;\nconst NEXT_MENTION = /\\brel\\s*=\\s*\"?[^\";,]*\\bnext\\b/i;\n\nfunction isTokenChar(ch: string): boolean {\n\treturn TOKEN_CHAR.test(ch);\n}\n\n/**\n * Tokenizer for RFC 8288 Link headers. URI references are read between `<` and `>`, so commas and\n * semicolons inside a URL are not treated as separators. Quoted parameter values support backslash\n * escapes. Malformed link-values are skipped; the scan resumes at the next `<`.\n */\nfunction scanLinkHeader(value: string): LinkScan {\n\tconst links: Record<string, string> = {};\n\tlet malformedNext = false;\n\tconst n = value.length;\n\tlet i = 0;\n\n\tconst skipSpaces = (): void => {\n\t\twhile (i < n && (value.charAt(i) === \" \" || value.charAt(i) === \"\\t\")) {\n\t\t\ti += 1;\n\t\t}\n\t};\n\n\twhile (i < n) {\n\t\tskipSpaces();\n\t\tif (i >= n) {\n\t\t\tbreak;\n\t\t}\n\t\tif (value.charAt(i) === \",\") {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst linkStart = i;\n\t\tlet target: string | undefined;\n\t\tif (value.charAt(i) === \"<\") {\n\t\t\tconst close = value.indexOf(\">\", i + 1);\n\t\t\tif (close !== -1) {\n\t\t\t\ttarget = value.slice(i + 1, close).trim();\n\t\t\t\ti = close + 1;\n\t\t\t}\n\t\t}\n\n\t\tlet ok = target !== undefined;\n\t\tconst rels: string[] = [];\n\n\t\twhile (ok) {\n\t\t\tskipSpaces();\n\t\t\tif (i >= n) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst ch = value.charAt(i);\n\t\t\tif (ch === \",\") {\n\t\t\t\ti += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (ch !== \";\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tskipSpaces();\n\n\t\t\tconst nameStart = i;\n\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tconst name = value.slice(nameStart, i).toLowerCase();\n\t\t\tif (name === \"\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tskipSpaces();\n\n\t\t\tlet paramValue: string | undefined;\n\t\t\tif (value.charAt(i) === \"=\") {\n\t\t\t\ti += 1;\n\t\t\t\tskipSpaces();\n\t\t\t\tif (value.charAt(i) === '\"') {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tlet quoted = \"\";\n\t\t\t\t\tlet closed = false;\n\t\t\t\t\twhile (i < n) {\n\t\t\t\t\t\tconst c = value.charAt(i);\n\t\t\t\t\t\tif (c === \"\\\\\" && i + 1 < n) {\n\t\t\t\t\t\t\tquoted += value.charAt(i + 1);\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t\tif (c === '\"') {\n\t\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tquoted += c;\n\t\t\t\t\t}\n\t\t\t\t\tif (!closed) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = quoted;\n\t\t\t\t} else {\n\t\t\t\t\tconst valueStart = i;\n\t\t\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t}\n\t\t\t\t\tif (i === valueStart) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = value.slice(valueStart, i);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (name === \"rel\" && paramValue !== undefined) {\n\t\t\t\trels.push(...paramValue.split(/\\s+/).filter((rel) => rel !== \"\"));\n\t\t\t}\n\t\t}\n\n\t\tif (ok && target !== undefined) {\n\t\t\tfor (const rel of rels) {\n\t\t\t\tconst key = rel.toLowerCase();\n\t\t\t\tif (key !== \"__proto__\" && !Object.hasOwn(links, key)) {\n\t\t\t\t\tlinks[key] = target;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmalformedNext = malformedNext || NEXT_MENTION.test(value.slice(linkStart));\n\t\t\tconst nextLink = value.indexOf(\"<\", linkStart + 1);\n\t\t\ti = nextLink === -1 ? n : nextLink;\n\t\t}\n\t}\n\n\treturn { links, malformedNext };\n}\n\n/**\n * Parses an RFC 8288 `Link` header value into a map from relation type to URI reference.\n *\n * Handles quoted and unquoted `rel` values, several relation types in one `rel`, several links\n * separated by commas, additional parameters and URIs that contain commas or semicolons.\n * Relation names are lower-cased. For duplicate relations the first link wins. Malformed\n * link-values are ignored.\n */\nexport function parseLinkHeader(value: string): Record<string, string> {\n\treturn scanLinkHeader(value).links;\n}\n\n/**\n * Returns the URI reference of the `rel=\"next\"` link from a `Link` header, or `undefined`.\n * The URI may be relative; resolve it against the request URL.\n *\n * Throws a `PaginationError` with code `INVALID_LINK` when a malformed link-value mentions\n * `rel=\"next\"` and no valid next link was found. Otherwise a broken header would look like the\n * last page and silently truncate the data.\n */\nexport function nextFromLinkHeader(headers: {\n\tget(name: string): string | null;\n}): string | undefined {\n\tconst raw = headers.get(\"link\");\n\tif (raw === null) {\n\t\treturn undefined;\n\t}\n\tconst { links, malformedNext } = scanLinkHeader(raw);\n\tif (Object.hasOwn(links, \"next\")) {\n\t\treturn links.next;\n\t}\n\tif (malformedNext) {\n\t\tthrow new PaginationError(\n\t\t\t\"INVALID_LINK\",\n\t\t\t0,\n\t\t\t'Malformed Link header: rel=\"next\" could not be parsed',\n\t\t);\n\t}\n\treturn undefined;\n}\n","/**\n * Returns a URL that is safe to put into an error message: credentials are removed, query values\n * are replaced with `***` (keys are kept) and fragments are dropped.\n */\nexport function maskUrl(input: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(input);\n\t} catch {\n\t\treturn \"<invalid URL>\";\n\t}\n\tconst origin = url.origin === \"null\" ? url.protocol : url.origin;\n\tconst keys = [...new Set(url.searchParams.keys())];\n\tconst query =\n\t\tkeys.length > 0 ? `?${keys.map((key) => `${encodeURIComponent(key)}=***`).join(\"&\")}` : \"\";\n\treturn `${origin}${url.pathname}${query}`;\n}\n","import { PaginationError } from \"./errors.js\";\nimport { nextFromLinkHeader } from \"./link-header.js\";\nimport { maskUrl } from \"./mask-url.js\";\nimport type { FetchPage } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst DEFAULT_RETRY_AFTER_MS = 1_000;\nconst MAX_RETRY_AFTER_MS = 60_000;\n\nexport interface FetchPagerOptions<T> {\n\t/** Fetch implementation to use. Defaults to `globalThis.fetch`. */\n\tfetch?: typeof fetch;\n\t/** URL of the first page. The cursor of every later page is a full URL. */\n\turl: string | URL;\n\t/** Maps the parsed JSON body to the items of the page. */\n\textract: (body: unknown) => T[];\n\t/**\n\t * Returns the URL of the next page, or `null` when there is none. Relative URLs are resolved\n\t * against the request URL. Defaults to the `rel=\"next\"` link of the `Link` header.\n\t */\n\tnextFromResponse?: (res: Response, body: unknown) => string | null | undefined;\n\t/**\n\t * Follow next URLs on a different origin than `url`. Defaults to `false`. Keep it `false`:\n\t * a compromised server could otherwise redirect the client to a foreign host and receive\n\t * credentials that the injected `fetch` attaches.\n\t */\n\tallowCrossOriginNext?: boolean;\n}\n\n/**\n * Parses the `Retry-After` value (delay in seconds or HTTP date) into milliseconds, capped at\n * 60 seconds. A missing or unparsable value falls back to one second.\n */\nfunction retryAfterMs(header: string | null, now: number): number {\n\tif (header === null) {\n\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t}\n\tconst value = header.trim();\n\tlet ms: number;\n\tif (/^\\d+$/.test(value)) {\n\t\tms = Number(value) * 1000;\n\t} else {\n\t\tconst date = Date.parse(value);\n\t\tif (Number.isNaN(date)) {\n\t\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t\t}\n\t\tms = Math.max(0, date - now);\n\t}\n\treturn Math.min(ms, MAX_RETRY_AFTER_MS);\n}\n\n/** Waits for `ms`, rejecting with the signal's reason if the signal aborts first. */\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tif (signal.aborted) {\n\t\t\treject(signal.reason);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => {\n\t\t\tclearTimeout(timer);\n\t\t\treject(signal.reason);\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Creates a `fetchPage` function for `paginate()` that follows URL-based cursors.\n *\n * - Next URLs come from the `Link` header (RFC 8288) or from `nextFromResponse`.\n * - Next URLs on a different origin than `url` are rejected with `INVALID_LINK`, unless\n * `allowCrossOriginNext` is set.\n * - HTTP 429 waits for `Retry-After` (seconds or HTTP date, capped at 60 s) and retries the same\n * page up to 3 times, then throws `RATE_LIMITED`. The wait counts towards the page timeout.\n * - Other non-2xx responses throw a plain `Error` that contains only the status code.\n *\n * Errors thrown here have `page: 0`; `paginate()` sets the real page number.\n */\nexport function createFetchPager<T>(options: FetchPagerOptions<T>): FetchPage<T> {\n\tconst doFetch = options.fetch ?? globalThis.fetch;\n\tif (typeof doFetch !== \"function\") {\n\t\tthrow new TypeError(\"No fetch implementation available; pass options.fetch\");\n\t}\n\tconst startUrl = new URL(options.url);\n\tconst startOrigin = startUrl.origin;\n\tconst allowCrossOrigin = options.allowCrossOriginNext === true;\n\tconst extract = options.extract;\n\tconst nextFromResponse = options.nextFromResponse;\n\n\tconst resolveNext = (raw: string | null | undefined, base: string): string | null => {\n\t\tif (raw === null || raw === undefined || raw === \"\") {\n\t\t\treturn null;\n\t\t}\n\t\tlet next: URL;\n\t\ttry {\n\t\t\tnext = new URL(raw, base);\n\t\t} catch {\n\t\t\tthrow new PaginationError(\"INVALID_LINK\", 0, \"Next link is not a valid URL\");\n\t\t}\n\t\tif (!allowCrossOrigin && next.origin !== startOrigin) {\n\t\t\tthrow new PaginationError(\n\t\t\t\t\"INVALID_LINK\",\n\t\t\t\t0,\n\t\t\t\t`Next link points to another origin (${next.origin}) and was not followed. ` +\n\t\t\t\t\t`Set allowCrossOriginNext to allow it. Link: ${maskUrl(next.href)}`,\n\t\t\t);\n\t\t}\n\t\treturn next.href;\n\t};\n\n\treturn async (cursor, signal) => {\n\t\tconst target = cursor === undefined ? startUrl.href : cursor;\n\t\tlet retries = 0;\n\n\t\tfor (;;) {\n\t\t\tconst res = await doFetch(target, { signal });\n\n\t\t\tif (res.status === 429) {\n\t\t\t\tawait res.body?.cancel().catch(() => undefined);\n\t\t\t\tif (retries >= MAX_RETRIES) {\n\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\"RATE_LIMITED\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t`Still rate limited (HTTP 429) after ${MAX_RETRIES} retries`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tretries += 1;\n\t\t\t\tawait sleep(retryAfterMs(res.headers.get(\"retry-after\"), Date.now()), signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`Request failed with HTTP ${res.status}`);\n\t\t\t}\n\n\t\t\tconst body: unknown = await res.json();\n\t\t\tconst items = extract(body);\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\tthrow new TypeError(\"extract() must return an array of items\");\n\t\t\t}\n\t\t\tconst raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);\n\t\t\treturn { items, nextCursor: resolveNext(raw, target) };\n\t\t}\n\t};\n}\n","import { PaginationError } from \"./errors.js\";\nimport type { FetchPage, Page, PaginateOptions } from \"./types.js\";\n\nconst DEFAULT_MAX_PAGES = 100;\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_EMPTY_PAGES = 3;\n\ninterface Limits {\n\tmaxPages: number;\n\tmaxItems: number;\n\ttimeoutMs: number;\n\ttotalTimeoutMs: number;\n}\n\nfunction resolveLimits<T>(options: PaginateOptions<T>): Limits {\n\tconst maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;\n\tconst maxItems = options.maxItems ?? Number.POSITIVE_INFINITY;\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst totalTimeoutMs = options.totalTimeoutMs ?? Number.POSITIVE_INFINITY;\n\n\tif (!(maxPages === Number.POSITIVE_INFINITY || (Number.isInteger(maxPages) && maxPages >= 1))) {\n\t\tthrow new RangeError(\"maxPages must be a positive integer or Infinity\");\n\t}\n\tif (!(maxItems === Number.POSITIVE_INFINITY || (Number.isInteger(maxItems) && maxItems >= 0))) {\n\t\tthrow new RangeError(\"maxItems must be a non-negative integer or Infinity\");\n\t}\n\tif (!(timeoutMs > 0)) {\n\t\tthrow new RangeError(\"timeoutMs must be greater than 0\");\n\t}\n\tif (!(totalTimeoutMs > 0)) {\n\t\tthrow new RangeError(\"totalTimeoutMs must be greater than 0\");\n\t}\n\treturn { maxPages, maxItems, timeoutMs, totalTimeoutMs };\n}\n\n/** `undefined`, `null` and `\"\"` mean \"no next page\". */\nfunction normalizeCursor(value: string | null | undefined): string | undefined {\n\treturn value === undefined || value === null || value === \"\" ? undefined : value;\n}\n\n/** Adapters without page context throw with page 0; this fills in the real page number. */\nfunction withPage(error: unknown, page: number): unknown {\n\tif (error instanceof PaginationError && error.page === 0) {\n\t\treturn new PaginationError(error.code, page, error.message, { cause: error });\n\t}\n\treturn error;\n}\n\nasync function* iterate<T>(\n\tfetchPage: FetchPage<T>,\n\tlimits: Limits,\n\tuserSignal: AbortSignal | undefined,\n): AsyncGenerator<T, void, undefined> {\n\tconst run = new AbortController();\n\tconst inflight: { ctl?: AbortController } = {};\n\tconst seen = new Set<string>();\n\tlet pageNo = 1;\n\tlet cursor: string | undefined;\n\tlet emitted = 0;\n\tlet emptyStreak = 0;\n\tlet totalTimer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst onUserAbort = (): void => {\n\t\trun.abort(\n\t\t\tnew PaginationError(\"ABORTED\", pageNo, \"Pagination was aborted\", {\n\t\t\t\tcause: userSignal?.reason,\n\t\t\t}),\n\t\t);\n\t};\n\n\t/**\n\t * Requests one page. The page signal is aborted when the run is aborted (user signal, total\n\t * timeout or iterator return) and after `timeoutMs`. The promise rejects as soon as the signal\n\t * fires, even if `fetchPage` ignores the signal.\n\t */\n\tconst request = (pageCursor: string | undefined, page: number): Promise<Page<T>> =>\n\t\tnew Promise<Page<T>>((resolve, reject) => {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\treject(run.signal.reason);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst ctl = new AbortController();\n\t\t\tinflight.ctl = ctl;\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst onParentAbort = (): void => ctl.abort(run.signal.reason);\n\t\t\tconst onPageAbort = (): void => settle(() => reject(ctl.signal.reason));\n\t\t\tconst settle = (done: () => void): void => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\trun.signal.removeEventListener(\"abort\", onParentAbort);\n\t\t\t\tctl.signal.removeEventListener(\"abort\", onPageAbort);\n\t\t\t\tdone();\n\t\t\t};\n\n\t\t\trun.signal.addEventListener(\"abort\", onParentAbort, { once: true });\n\t\t\tctl.signal.addEventListener(\"abort\", onPageAbort, { once: true });\n\t\t\tif (Number.isFinite(limits.timeoutMs)) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tctl.abort(\n\t\t\t\t\t\tnew PaginationError(\n\t\t\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\t\t\tpage,\n\t\t\t\t\t\t\t`Page ${page} timed out after ${limits.timeoutMs} ms`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}, limits.timeoutMs);\n\t\t\t}\n\n\t\t\tlet work: Promise<Page<T>>;\n\t\t\ttry {\n\t\t\t\twork = Promise.resolve(fetchPage(pageCursor, ctl.signal));\n\t\t\t} catch (error) {\n\t\t\t\twork = Promise.reject(error);\n\t\t\t}\n\t\t\twork.then(\n\t\t\t\t(result) => settle(() => resolve(result)),\n\t\t\t\t(error: unknown) => settle(() => reject(withPage(error, page))),\n\t\t\t);\n\t\t});\n\n\tif (userSignal?.aborted) {\n\t\tonUserAbort();\n\t} else {\n\t\tuserSignal?.addEventListener(\"abort\", onUserAbort, { once: true });\n\t}\n\tif (Number.isFinite(limits.totalTimeoutMs)) {\n\t\ttotalTimer = setTimeout(() => {\n\t\t\trun.abort(\n\t\t\t\tnew PaginationError(\n\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Total timeout of ${limits.totalTimeoutMs} ms exceeded`,\n\t\t\t\t),\n\t\t\t);\n\t\t}, limits.totalTimeoutMs);\n\t}\n\n\ttry {\n\t\twhile (emitted < limits.maxItems) {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\tthrow run.signal.reason;\n\t\t\t}\n\n\t\t\tconst result = await request(cursor, pageNo);\n\t\t\tif (!Array.isArray(result.items)) {\n\t\t\t\tthrow new TypeError(\"fetchPage must resolve to an object with an items array\");\n\t\t\t}\n\t\t\tconst next = normalizeCursor(result.nextCursor);\n\n\t\t\tif (next !== undefined && seen.has(next)) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"CURSOR_LOOP\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Server returned a cursor that was already used (page ${pageNo}); stopping`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (result.items.length === 0) {\n\t\t\t\tif (next !== undefined) {\n\t\t\t\t\temptyStreak += 1;\n\t\t\t\t\tif (emptyStreak >= MAX_EMPTY_PAGES) {\n\t\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\t\"EMPTY_PAGES_LOOP\",\n\t\t\t\t\t\t\tpageNo,\n\t\t\t\t\t\t\t`${MAX_EMPTY_PAGES} consecutive empty pages with a next cursor (page ${pageNo}); stopping`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\temptyStreak = 0;\n\t\t\t}\n\n\t\t\tfor (const item of result.items) {\n\t\t\t\tif (run.signal.aborted) {\n\t\t\t\t\tthrow run.signal.reason;\n\t\t\t\t}\n\t\t\t\tyield item;\n\t\t\t\temitted += 1;\n\t\t\t\tif (emitted >= limits.maxItems) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (next === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (pageNo >= limits.maxPages) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"PAGE_LIMIT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Page limit of ${limits.maxPages} reached while more pages are available`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tseen.add(next);\n\t\t\tcursor = next;\n\t\t\tpageNo += 1;\n\t\t}\n\t} finally {\n\t\tclearTimeout(totalTimer);\n\t\tuserSignal?.removeEventListener(\"abort\", onUserAbort);\n\t\t// Cancels the open request when the consumer stops early (break, return) or on errors.\n\t\tinflight.ctl?.abort();\n\t\trun.abort();\n\t}\n}\n\n/**\n * Iterates over all items of a paginated API.\n *\n * Guards: repeated cursors (`CURSOR_LOOP`), a page limit (`PAGE_LIMIT`), endless empty pages\n * (`EMPTY_PAGES_LOOP`), per-page and total timeouts (`TIMEOUT`) and an abort signal (`ABORTED`).\n * Reaching `maxItems` stops without error.\n *\n * Breaking out of a `for await` loop calls `return()`, which aborts the signal of any request\n * that is still open.\n *\n * Options are validated when `paginate()` is called. Iteration starts on the first `next()`.\n */\nexport function paginate<T>(options: PaginateOptions<T>): AsyncGenerator<T, void, undefined> {\n\tif (typeof options.fetchPage !== \"function\") {\n\t\tthrow new TypeError(\"fetchPage must be a function\");\n\t}\n\treturn iterate(options.fetchPage, resolveLimits(options), options.signal);\n}\n"],"mappings":";;;;;;;;;AAgBA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CACA;CAEA,YACC,MACA,MACA,SACA,SACC;EACD,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,OAAO;CACb;AACD;;ACvBA,MAAM,aAAa;AACnB,MAAM,eAAe;AAErB,SAAS,YAAY,IAAqB;CACzC,OAAO,WAAW,KAAK,EAAE;AAC1B;;;;;;AAOA,SAAS,eAAe,OAAyB;CAChD,MAAM,QAAgC,CAAC;CACvC,IAAI,gBAAgB;CACpB,MAAM,IAAI,MAAM;CAChB,IAAI,IAAI;CAER,MAAM,mBAAyB;EAC9B,OAAO,IAAI,MAAM,MAAM,OAAO,CAAC,MAAM,OAAO,MAAM,OAAO,CAAC,MAAM,MAC/D,KAAK;CAEP;CAEA,OAAO,IAAI,GAAG;EACb,WAAW;EACX,IAAI,KAAK,GACR;EAED,IAAI,MAAM,OAAO,CAAC,MAAM,KAAK;GAC5B,KAAK;GACL;EACD;EAEA,MAAM,YAAY;EAClB,IAAI;EACJ,IAAI,MAAM,OAAO,CAAC,MAAM,KAAK;GAC5B,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,CAAC;GACtC,IAAI,UAAU,IAAI;IACjB,SAAS,MAAM,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK;IACxC,IAAI,QAAQ;GACb;EACD;EAEA,IAAI,KAAK,WAAW,KAAA;EACpB,MAAM,OAAiB,CAAC;EAExB,OAAO,IAAI;GACV,WAAW;GACX,IAAI,KAAK,GACR;GAED,MAAM,KAAK,MAAM,OAAO,CAAC;GACzB,IAAI,OAAO,KAAK;IACf,KAAK;IACL;GACD;GACA,IAAI,OAAO,KAAK;IACf,KAAK;IACL;GACD;GACA,KAAK;GACL,WAAW;GAEX,MAAM,YAAY;GAClB,OAAO,IAAI,KAAK,YAAY,MAAM,OAAO,CAAC,CAAC,GAC1C,KAAK;GAEN,MAAM,OAAO,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,YAAY;GACnD,IAAI,SAAS,IAAI;IAChB,KAAK;IACL;GACD;GACA,WAAW;GAEX,IAAI;GACJ,IAAI,MAAM,OAAO,CAAC,MAAM,KAAK;IAC5B,KAAK;IACL,WAAW;IACX,IAAI,MAAM,OAAO,CAAC,MAAM,MAAK;KAC5B,KAAK;KACL,IAAI,SAAS;KACb,IAAI,SAAS;KACb,OAAO,IAAI,GAAG;MACb,MAAM,IAAI,MAAM,OAAO,CAAC;MACxB,IAAI,MAAM,QAAQ,IAAI,IAAI,GAAG;OAC5B,UAAU,MAAM,OAAO,IAAI,CAAC;OAC5B,KAAK;OACL;MACD;MACA,KAAK;MACL,IAAI,MAAM,MAAK;OACd,SAAS;OACT;MACD;MACA,UAAU;KACX;KACA,IAAI,CAAC,QAAQ;MACZ,KAAK;MACL;KACD;KACA,aAAa;IACd,OAAO;KACN,MAAM,aAAa;KACnB,OAAO,IAAI,KAAK,YAAY,MAAM,OAAO,CAAC,CAAC,GAC1C,KAAK;KAEN,IAAI,MAAM,YAAY;MACrB,KAAK;MACL;KACD;KACA,aAAa,MAAM,MAAM,YAAY,CAAC;IACvC;GACD;GAEA,IAAI,SAAS,SAAS,eAAe,KAAA,GACpC,KAAK,KAAK,GAAG,WAAW,MAAM,KAAK,CAAC,CAAC,QAAQ,QAAQ,QAAQ,EAAE,CAAC;EAElE;EAEA,IAAI,MAAM,WAAW,KAAA,GACpB,KAAK,MAAM,OAAO,MAAM;GACvB,MAAM,MAAM,IAAI,YAAY;GAC5B,IAAI,QAAQ,eAAe,CAAC,OAAO,OAAO,OAAO,GAAG,GACnD,MAAM,OAAO;EAEf;OACM;GACN,gBAAgB,iBAAiB,aAAa,KAAK,MAAM,MAAM,SAAS,CAAC;GACzE,MAAM,WAAW,MAAM,QAAQ,KAAK,YAAY,CAAC;GACjD,IAAI,aAAa,KAAK,IAAI;EAC3B;CACD;CAEA,OAAO;EAAE;EAAO;CAAc;AAC/B;;;;;;;;;AAUA,SAAgB,gBAAgB,OAAuC;CACtE,OAAO,eAAe,KAAK,CAAC,CAAC;AAC9B;;;;;;;;;AAUA,SAAgB,mBAAmB,SAEZ;CACtB,MAAM,MAAM,QAAQ,IAAI,MAAM;CAC9B,IAAI,QAAQ,MACX;CAED,MAAM,EAAE,OAAO,kBAAkB,eAAe,GAAG;CACnD,IAAI,OAAO,OAAO,OAAO,MAAM,GAC9B,OAAO,MAAM;CAEd,IAAI,eACH,MAAM,IAAI,gBACT,gBACA,GACA,yDACD;AAGF;;;;;;ACpLA,SAAgB,QAAQ,OAAuB;CAC9C,IAAI;CACJ,IAAI;EACH,MAAM,IAAI,IAAI,KAAK;CACpB,QAAQ;EACP,OAAO;CACR;CACA,MAAM,SAAS,IAAI,WAAW,SAAS,IAAI,WAAW,IAAI;CAC1D,MAAM,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,aAAa,KAAK,CAAC,CAAC;CACjD,MAAM,QACL,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,QAAQ,GAAG,mBAAmB,GAAG,EAAE,KAAK,CAAC,CAAC,KAAK,GAAG,MAAM;CACzF,OAAO,GAAG,SAAS,IAAI,WAAW;AACnC;;ACXA,MAAM,cAAc;AACpB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;;;;;AA0B3B,SAAS,aAAa,QAAuB,KAAqB;CACjE,IAAI,WAAW,MACd,OAAO;CAER,MAAM,QAAQ,OAAO,KAAK;CAC1B,IAAI;CACJ,IAAI,QAAQ,KAAK,KAAK,GACrB,KAAK,OAAO,KAAK,IAAI;MACf;EACN,MAAM,OAAO,KAAK,MAAM,KAAK;EAC7B,IAAI,OAAO,MAAM,IAAI,GACpB,OAAO;EAER,KAAK,KAAK,IAAI,GAAG,OAAO,GAAG;CAC5B;CACA,OAAO,KAAK,IAAI,IAAI,kBAAkB;AACvC;;AAGA,SAAS,MAAM,IAAY,QAAoC;CAC9D,OAAO,IAAI,SAAe,SAAS,WAAW;EAC7C,IAAI,OAAO,SAAS;GACnB,OAAO,OAAO,MAAM;GACpB;EACD;EACA,MAAM,gBAAsB;GAC3B,aAAa,KAAK;GAClB,OAAO,OAAO,MAAM;EACrB;EACA,MAAM,QAAQ,iBAAiB;GAC9B,OAAO,oBAAoB,SAAS,OAAO;GAC3C,QAAQ;EACT,GAAG,EAAE;EACL,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CACzD,CAAC;AACF;;;;;;;;;;;;;AAcA,SAAgB,iBAAoB,SAA6C;CAChF,MAAM,UAAU,QAAQ,SAAS,WAAW;CAC5C,IAAI,OAAO,YAAY,YACtB,MAAM,IAAI,UAAU,uDAAuD;CAE5E,MAAM,WAAW,IAAI,IAAI,QAAQ,GAAG;CACpC,MAAM,cAAc,SAAS;CAC7B,MAAM,mBAAmB,QAAQ,yBAAyB;CAC1D,MAAM,UAAU,QAAQ;CACxB,MAAM,mBAAmB,QAAQ;CAEjC,MAAM,eAAe,KAAgC,SAAgC;EACpF,IAAI,QAAQ,QAAQ,QAAQ,KAAA,KAAa,QAAQ,IAChD,OAAO;EAER,IAAI;EACJ,IAAI;GACH,OAAO,IAAI,IAAI,KAAK,IAAI;EACzB,QAAQ;GACP,MAAM,IAAI,gBAAgB,gBAAgB,GAAG,8BAA8B;EAC5E;EACA,IAAI,CAAC,oBAAoB,KAAK,WAAW,aACxC,MAAM,IAAI,gBACT,gBACA,GACA,uCAAuC,KAAK,OAAO,sEACH,QAAQ,KAAK,IAAI,GAClE;EAED,OAAO,KAAK;CACb;CAEA,OAAO,OAAO,QAAQ,WAAW;EAChC,MAAM,SAAS,WAAW,KAAA,IAAY,SAAS,OAAO;EACtD,IAAI,UAAU;EAEd,SAAS;GACR,MAAM,MAAM,MAAM,QAAQ,QAAQ,EAAE,OAAO,CAAC;GAE5C,IAAI,IAAI,WAAW,KAAK;IACvB,MAAM,IAAI,MAAM,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;IAC9C,IAAI,WAAW,aACd,MAAM,IAAI,gBACT,gBACA,GACA,uCAAuC,YAAY,SACpD;IAED,WAAW;IACX,MAAM,MAAM,aAAa,IAAI,QAAQ,IAAI,aAAa,GAAG,KAAK,IAAI,CAAC,GAAG,MAAM;IAC5E;GACD;GAEA,IAAI,CAAC,IAAI,IACR,MAAM,IAAI,MAAM,4BAA4B,IAAI,QAAQ;GAGzD,MAAM,OAAgB,MAAM,IAAI,KAAK;GACrC,MAAM,QAAQ,QAAQ,IAAI;GAC1B,IAAI,CAAC,MAAM,QAAQ,KAAK,GACvB,MAAM,IAAI,UAAU,yCAAyC;GAE9D,MAAM,MAAM,mBAAmB,iBAAiB,KAAK,IAAI,IAAI,mBAAmB,IAAI,OAAO;GAC3F,OAAO;IAAE;IAAO,YAAY,YAAY,KAAK,MAAM;GAAE;EACtD;CACD;AACD;;ACjJA,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AASxB,SAAS,cAAiB,SAAqC;CAC9D,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,WAAW,QAAQ,YAAY,OAAO;CAC5C,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,iBAAiB,QAAQ,kBAAkB,OAAO;CAExD,IAAI,EAAE,aAAa,OAAO,qBAAsB,OAAO,UAAU,QAAQ,KAAK,YAAY,IACzF,MAAM,IAAI,WAAW,iDAAiD;CAEvE,IAAI,EAAE,aAAa,OAAO,qBAAsB,OAAO,UAAU,QAAQ,KAAK,YAAY,IACzF,MAAM,IAAI,WAAW,qDAAqD;CAE3E,IAAI,EAAE,YAAY,IACjB,MAAM,IAAI,WAAW,kCAAkC;CAExD,IAAI,EAAE,iBAAiB,IACtB,MAAM,IAAI,WAAW,uCAAuC;CAE7D,OAAO;EAAE;EAAU;EAAU;EAAW;CAAe;AACxD;;AAGA,SAAS,gBAAgB,OAAsD;CAC9E,OAAO,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,KAAK,KAAA,IAAY;AAC5E;;AAGA,SAAS,SAAS,OAAgB,MAAuB;CACxD,IAAI,iBAAiB,mBAAmB,MAAM,SAAS,GACtD,OAAO,IAAI,gBAAgB,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;CAE7E,OAAO;AACR;AAEA,gBAAgB,QACf,WACA,QACA,YACqC;CACrC,MAAM,MAAM,IAAI,gBAAgB;CAChC,MAAM,WAAsC,CAAC;CAC7C,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI,SAAS;CACb,IAAI;CACJ,IAAI,UAAU;CACd,IAAI,cAAc;CAClB,IAAI;CAEJ,MAAM,oBAA0B;EAC/B,IAAI,MACH,IAAI,gBAAgB,WAAW,QAAQ,0BAA0B,EAChE,OAAO,YAAY,OACpB,CAAC,CACF;CACD;;;;;;CAOA,MAAM,WAAW,YAAgC,SAChD,IAAI,SAAkB,SAAS,WAAW;EACzC,IAAI,IAAI,OAAO,SAAS;GACvB,OAAO,IAAI,OAAO,MAAM;GACxB;EACD;EACA,MAAM,MAAM,IAAI,gBAAgB;EAChC,SAAS,MAAM;EACf,IAAI;EAEJ,MAAM,sBAA4B,IAAI,MAAM,IAAI,OAAO,MAAM;EAC7D,MAAM,oBAA0B,aAAa,OAAO,IAAI,OAAO,MAAM,CAAC;EACtE,MAAM,UAAU,SAA2B;GAC1C,aAAa,KAAK;GAClB,IAAI,OAAO,oBAAoB,SAAS,aAAa;GACrD,IAAI,OAAO,oBAAoB,SAAS,WAAW;GACnD,KAAK;EACN;EAEA,IAAI,OAAO,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;EAClE,IAAI,OAAO,iBAAiB,SAAS,aAAa,EAAE,MAAM,KAAK,CAAC;EAChE,IAAI,OAAO,SAAS,OAAO,SAAS,GACnC,QAAQ,iBAAiB;GACxB,IAAI,MACH,IAAI,gBACH,WACA,MACA,QAAQ,KAAK,mBAAmB,OAAO,UAAU,IAClD,CACD;EACD,GAAG,OAAO,SAAS;EAGpB,IAAI;EACJ,IAAI;GACH,OAAO,QAAQ,QAAQ,UAAU,YAAY,IAAI,MAAM,CAAC;EACzD,SAAS,OAAO;GACf,OAAO,QAAQ,OAAO,KAAK;EAC5B;EACA,KAAK,MACH,WAAW,aAAa,QAAQ,MAAM,CAAC,IACvC,UAAmB,aAAa,OAAO,SAAS,OAAO,IAAI,CAAC,CAAC,CAC/D;CACD,CAAC;CAEF,IAAI,YAAY,SACf,YAAY;MAEZ,YAAY,iBAAiB,SAAS,aAAa,EAAE,MAAM,KAAK,CAAC;CAElE,IAAI,OAAO,SAAS,OAAO,cAAc,GACxC,aAAa,iBAAiB;EAC7B,IAAI,MACH,IAAI,gBACH,WACA,QACA,oBAAoB,OAAO,eAAe,aAC3C,CACD;CACD,GAAG,OAAO,cAAc;CAGzB,IAAI;EACH,OAAO,UAAU,OAAO,UAAU;GACjC,IAAI,IAAI,OAAO,SACd,MAAM,IAAI,OAAO;GAGlB,MAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM;GAC3C,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,GAC9B,MAAM,IAAI,UAAU,yDAAyD;GAE9E,MAAM,OAAO,gBAAgB,OAAO,UAAU;GAE9C,IAAI,SAAS,KAAA,KAAa,KAAK,IAAI,IAAI,GACtC,MAAM,IAAI,gBACT,eACA,QACA,wDAAwD,OAAO,YAChE;GAGD,IAAI,OAAO,MAAM,WAAW,GACvB;QAAA,SAAS,KAAA,GAAW;KACvB,eAAe;KACf,IAAI,eAAe,iBAClB,MAAM,IAAI,gBACT,oBACA,QACA,GAAG,gBAAgB,oDAAoD,OAAO,YAC/E;IAEF;UAEA,cAAc;GAGf,KAAK,MAAM,QAAQ,OAAO,OAAO;IAChC,IAAI,IAAI,OAAO,SACd,MAAM,IAAI,OAAO;IAElB,MAAM;IACN,WAAW;IACX,IAAI,WAAW,OAAO,UACrB;GAEF;GAEA,IAAI,SAAS,KAAA,GACZ;GAED,IAAI,UAAU,OAAO,UACpB,MAAM,IAAI,gBACT,cACA,QACA,iBAAiB,OAAO,SAAS,wCAClC;GAED,KAAK,IAAI,IAAI;GACb,SAAS;GACT,UAAU;EACX;CACD,UAAU;EACT,aAAa,UAAU;EACvB,YAAY,oBAAoB,SAAS,WAAW;EAEpD,SAAS,KAAK,MAAM;EACpB,IAAI,MAAM;CACX;AACD;;;;;;;;;;;;;AAcA,SAAgB,SAAY,SAAiE;CAC5F,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,UAAU,8BAA8B;CAEnD,OAAO,QAAQ,QAAQ,WAAW,cAAc,OAAO,GAAG,QAAQ,MAAM;AACzE"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/link-header.ts","../src/mask-url.ts","../src/fetch-pager.ts","../src/paginate.ts"],"sourcesContent":["export type PaginationErrorCode =\n\t| \"CURSOR_LOOP\"\n\t| \"PAGE_LIMIT\"\n\t| \"RATE_LIMITED\"\n\t| \"TIMEOUT\"\n\t| \"ABORTED\"\n\t| \"EMPTY_PAGES_LOOP\"\n\t| \"INVALID_LINK\";\n\n/**\n * Error raised by the pagination guards.\n *\n * `page` is the 1-based number of the page whose request or response caused the error.\n * Adapters such as `createFetchPager` do not know the page number and use `0`; `paginate()`\n * replaces it with the real number before the error reaches the caller.\n */\nexport class PaginationError extends Error {\n\treadonly code: PaginationErrorCode;\n\treadonly page: number;\n\n\tconstructor(\n\t\tcode: PaginationErrorCode,\n\t\tpage: number,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.name = \"PaginationError\";\n\t\tthis.code = code;\n\t\tthis.page = page;\n\t}\n}\n","import { PaginationError } from \"./errors.js\";\n\ninterface LinkScan {\n\tlinks: Record<string, string>;\n\t/** True when a malformed link-value mentions `rel=\"next\"`. */\n\tmalformedNext: boolean;\n}\n\nconst TOKEN_CHAR = /[!#$%&'*+\\-.^_`|~0-9A-Za-z]/;\nconst NEXT_MENTION = /\\brel\\s*=\\s*\"?[^\";,]*\\bnext\\b/i;\n\nfunction isTokenChar(ch: string): boolean {\n\treturn TOKEN_CHAR.test(ch);\n}\n\n/**\n * Tokenizer for RFC 8288 Link headers. URI references are read between `<` and `>`, so commas and\n * semicolons inside a URL are not treated as separators. Quoted parameter values support backslash\n * escapes. Malformed link-values are skipped; the scan resumes at the next `<`.\n */\nfunction scanLinkHeader(value: string): LinkScan {\n\tconst links: Record<string, string> = {};\n\tlet malformedNext = false;\n\tconst n = value.length;\n\tlet i = 0;\n\n\tconst skipSpaces = (): void => {\n\t\twhile (i < n && (value.charAt(i) === \" \" || value.charAt(i) === \"\\t\")) {\n\t\t\ti += 1;\n\t\t}\n\t};\n\n\twhile (i < n) {\n\t\tskipSpaces();\n\t\tif (i >= n) {\n\t\t\tbreak;\n\t\t}\n\t\tif (value.charAt(i) === \",\") {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst linkStart = i;\n\t\tlet target: string | undefined;\n\t\tif (value.charAt(i) === \"<\") {\n\t\t\tconst close = value.indexOf(\">\", i + 1);\n\t\t\tif (close !== -1) {\n\t\t\t\ttarget = value.slice(i + 1, close).trim();\n\t\t\t\ti = close + 1;\n\t\t\t}\n\t\t}\n\n\t\tlet ok = target !== undefined;\n\t\tconst rels: string[] = [];\n\n\t\twhile (ok) {\n\t\t\tskipSpaces();\n\t\t\tif (i >= n) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst ch = value.charAt(i);\n\t\t\tif (ch === \",\") {\n\t\t\t\ti += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (ch !== \";\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tskipSpaces();\n\n\t\t\tconst nameStart = i;\n\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tconst name = value.slice(nameStart, i).toLowerCase();\n\t\t\tif (name === \"\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tskipSpaces();\n\n\t\t\tlet paramValue: string | undefined;\n\t\t\tif (value.charAt(i) === \"=\") {\n\t\t\t\ti += 1;\n\t\t\t\tskipSpaces();\n\t\t\t\tif (value.charAt(i) === '\"') {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tlet quoted = \"\";\n\t\t\t\t\tlet closed = false;\n\t\t\t\t\twhile (i < n) {\n\t\t\t\t\t\tconst c = value.charAt(i);\n\t\t\t\t\t\tif (c === \"\\\\\" && i + 1 < n) {\n\t\t\t\t\t\t\tquoted += value.charAt(i + 1);\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t\tif (c === '\"') {\n\t\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tquoted += c;\n\t\t\t\t\t}\n\t\t\t\t\tif (!closed) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = quoted;\n\t\t\t\t} else {\n\t\t\t\t\tconst valueStart = i;\n\t\t\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t}\n\t\t\t\t\tif (i === valueStart) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = value.slice(valueStart, i);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (name === \"rel\" && paramValue !== undefined) {\n\t\t\t\trels.push(...paramValue.split(/\\s+/).filter((rel) => rel !== \"\"));\n\t\t\t}\n\t\t}\n\n\t\tif (ok && target !== undefined) {\n\t\t\tfor (const rel of rels) {\n\t\t\t\tconst key = rel.toLowerCase();\n\t\t\t\tif (key !== \"__proto__\" && !Object.hasOwn(links, key)) {\n\t\t\t\t\tlinks[key] = target;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmalformedNext = malformedNext || NEXT_MENTION.test(value.slice(linkStart));\n\t\t\tconst nextLink = value.indexOf(\"<\", linkStart + 1);\n\t\t\ti = nextLink === -1 ? n : nextLink;\n\t\t}\n\t}\n\n\treturn { links, malformedNext };\n}\n\n/**\n * Parses an RFC 8288 `Link` header value into a map from relation type to URI reference.\n *\n * Handles quoted and unquoted `rel` values, several relation types in one `rel`, several links\n * separated by commas, additional parameters and URIs that contain commas or semicolons.\n * Relation names are lower-cased. For duplicate relations the first link wins. Malformed\n * link-values are ignored.\n */\nexport function parseLinkHeader(value: string): Record<string, string> {\n\treturn scanLinkHeader(value).links;\n}\n\n/**\n * Returns the URI reference of the `rel=\"next\"` link from a `Link` header, or `undefined`.\n * The URI may be relative; resolve it against the request URL.\n *\n * Throws a `PaginationError` with code `INVALID_LINK` when a malformed link-value mentions\n * `rel=\"next\"` and no valid next link was found. Otherwise a broken header would look like the\n * last page and silently truncate the data.\n */\nexport function nextFromLinkHeader(headers: {\n\tget(name: string): string | null;\n}): string | undefined {\n\tconst raw = headers.get(\"link\");\n\tif (raw === null) {\n\t\treturn undefined;\n\t}\n\tconst { links, malformedNext } = scanLinkHeader(raw);\n\tif (Object.hasOwn(links, \"next\")) {\n\t\treturn links.next;\n\t}\n\tif (malformedNext) {\n\t\tthrow new PaginationError(\n\t\t\t\"INVALID_LINK\",\n\t\t\t0,\n\t\t\t'Malformed Link header: rel=\"next\" could not be parsed',\n\t\t);\n\t}\n\treturn undefined;\n}\n","/**\n * Returns a URL that is safe to put into an error message: credentials are removed, query values\n * are replaced with `***` (keys are kept) and fragments are dropped.\n */\nexport function maskUrl(input: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(input);\n\t} catch {\n\t\treturn \"<invalid URL>\";\n\t}\n\tconst origin = url.origin === \"null\" ? url.protocol : url.origin;\n\tconst keys = [...new Set(url.searchParams.keys())];\n\tconst query =\n\t\tkeys.length > 0 ? `?${keys.map((key) => `${encodeURIComponent(key)}=***`).join(\"&\")}` : \"\";\n\treturn `${origin}${url.pathname}${query}`;\n}\n","import { PaginationError } from \"./errors.js\";\nimport { nextFromLinkHeader } from \"./link-header.js\";\nimport { maskUrl } from \"./mask-url.js\";\nimport type { FetchPage } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst DEFAULT_RETRY_AFTER_MS = 1_000;\nconst MAX_RETRY_AFTER_MS = 60_000;\n\nexport interface FetchPagerOptions<T> {\n\t/** Fetch implementation to use. Defaults to `globalThis.fetch`. */\n\tfetch?: typeof fetch;\n\t/** URL of the first page. The cursor of every later page is a full URL. */\n\turl: string | URL;\n\t/** Maps the parsed JSON body to the items of the page. */\n\textract: (body: unknown) => T[];\n\t/**\n\t * Returns the URL of the next page, or `null` when there is none. Relative URLs are resolved\n\t * against the request URL. Defaults to the `rel=\"next\"` link of the `Link` header.\n\t */\n\tnextFromResponse?: (res: Response, body: unknown) => string | null | undefined;\n\t/**\n\t * Follow next URLs on a different origin than `url`. Defaults to `false`. Keep it `false`:\n\t * a compromised server could otherwise redirect the client to a foreign host and receive\n\t * credentials that the injected `fetch` attaches.\n\t */\n\tallowCrossOriginNext?: boolean;\n}\n\n/**\n * Parses the `Retry-After` value (delay in seconds or HTTP date) into milliseconds, capped at\n * 60 seconds. A missing or unparsable value falls back to one second.\n */\nfunction retryAfterMs(header: string | null, now: number): number {\n\tif (header === null) {\n\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t}\n\tconst value = header.trim();\n\tlet ms: number;\n\tif (/^\\d+$/.test(value)) {\n\t\tms = Number(value) * 1000;\n\t} else {\n\t\tconst date = Date.parse(value);\n\t\tif (Number.isNaN(date)) {\n\t\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t\t}\n\t\tms = Math.max(0, date - now);\n\t}\n\treturn Math.min(ms, MAX_RETRY_AFTER_MS);\n}\n\n/** Waits for `ms`, rejecting with the signal's reason if the signal aborts first. */\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tif (signal.aborted) {\n\t\t\treject(signal.reason);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => {\n\t\t\tclearTimeout(timer);\n\t\t\treject(signal.reason);\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Creates a `fetchPage` function for `paginate()` that follows URL-based cursors.\n *\n * - Next URLs come from the `Link` header (RFC 8288) or from `nextFromResponse`.\n * - Next URLs on a different origin than `url` are rejected with `INVALID_LINK`, unless\n * `allowCrossOriginNext` is set.\n * - HTTP 429 waits for `Retry-After` (seconds or HTTP date, capped at 60 s) and retries the same\n * page up to 3 times, then throws `RATE_LIMITED`. The wait counts towards the page timeout.\n * - Other non-2xx responses throw a plain `Error` that contains only the status code.\n *\n * Errors thrown here have `page: 0`; `paginate()` sets the real page number.\n */\nexport function createFetchPager<T>(options: FetchPagerOptions<T>): FetchPage<T> {\n\tconst doFetch = options.fetch ?? globalThis.fetch;\n\tif (typeof doFetch !== \"function\") {\n\t\tthrow new TypeError(\"No fetch implementation available; pass options.fetch\");\n\t}\n\tconst startUrl = new URL(options.url);\n\tconst startOrigin = startUrl.origin;\n\tconst allowCrossOrigin = options.allowCrossOriginNext === true;\n\tconst extract = options.extract;\n\tconst nextFromResponse = options.nextFromResponse;\n\n\tconst resolveNext = (raw: string | null | undefined, base: string): string | null => {\n\t\tif (raw === null || raw === undefined || raw === \"\") {\n\t\t\treturn null;\n\t\t}\n\t\tlet next: URL;\n\t\ttry {\n\t\t\tnext = new URL(raw, base);\n\t\t} catch {\n\t\t\tthrow new PaginationError(\"INVALID_LINK\", 0, \"Next link is not a valid URL\");\n\t\t}\n\t\tif (!allowCrossOrigin && next.origin !== startOrigin) {\n\t\t\tthrow new PaginationError(\n\t\t\t\t\"INVALID_LINK\",\n\t\t\t\t0,\n\t\t\t\t`Next link points to another origin (${next.origin}) and was not followed. ` +\n\t\t\t\t\t`Set allowCrossOriginNext to allow it. Link: ${maskUrl(next.href)}`,\n\t\t\t);\n\t\t}\n\t\treturn next.href;\n\t};\n\n\treturn async (cursor, signal) => {\n\t\tconst target = cursor === undefined ? startUrl.href : cursor;\n\t\tlet retries = 0;\n\n\t\tfor (;;) {\n\t\t\tconst res = await doFetch(target, { signal });\n\n\t\t\tif (res.status === 429) {\n\t\t\t\tawait res.body?.cancel().catch(() => undefined);\n\t\t\t\tif (retries >= MAX_RETRIES) {\n\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\"RATE_LIMITED\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t`Still rate limited (HTTP 429) after ${MAX_RETRIES} retries`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tretries += 1;\n\t\t\t\tawait sleep(retryAfterMs(res.headers.get(\"retry-after\"), Date.now()), signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`Request failed with HTTP ${res.status}`);\n\t\t\t}\n\n\t\t\tlet body: unknown;\n\t\t\ttry {\n\t\t\t\tbody = await res.json();\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\t\t// Name the response without echoing its body, which may contain secrets.\n\t\t\t\t\tthrow new Error(`Response of HTTP ${res.status} is not valid JSON`, { cause: error });\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst items = extract(body);\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\tthrow new TypeError(\"extract() must return an array of items\");\n\t\t\t}\n\t\t\tconst raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);\n\t\t\treturn { items, nextCursor: resolveNext(raw, target) };\n\t\t}\n\t};\n}\n","import { PaginationError } from \"./errors.js\";\nimport type { FetchPage, Page, PaginateOptions } from \"./types.js\";\n\nconst DEFAULT_MAX_PAGES = 100;\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_EMPTY_PAGES = 3;\n\ninterface Limits {\n\tmaxPages: number;\n\tmaxItems: number;\n\ttimeoutMs: number;\n\ttotalTimeoutMs: number;\n}\n\nfunction resolveLimits<T>(options: PaginateOptions<T>): Limits {\n\tconst maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;\n\tconst maxItems = options.maxItems ?? Number.POSITIVE_INFINITY;\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst totalTimeoutMs = options.totalTimeoutMs ?? Number.POSITIVE_INFINITY;\n\n\tif (!(maxPages === Number.POSITIVE_INFINITY || (Number.isInteger(maxPages) && maxPages >= 1))) {\n\t\tthrow new RangeError(\"maxPages must be a positive integer or Infinity\");\n\t}\n\tif (!(maxItems === Number.POSITIVE_INFINITY || (Number.isInteger(maxItems) && maxItems >= 0))) {\n\t\tthrow new RangeError(\"maxItems must be a non-negative integer or Infinity\");\n\t}\n\tif (!(timeoutMs > 0)) {\n\t\tthrow new RangeError(\"timeoutMs must be greater than 0\");\n\t}\n\tif (!(totalTimeoutMs > 0)) {\n\t\tthrow new RangeError(\"totalTimeoutMs must be greater than 0\");\n\t}\n\treturn { maxPages, maxItems, timeoutMs, totalTimeoutMs };\n}\n\n/** `undefined`, `null` and `\"\"` mean \"no next page\". */\nfunction normalizeCursor(value: string | null | undefined): string | undefined {\n\treturn value === undefined || value === null || value === \"\" ? undefined : value;\n}\n\n/** Adapters without page context throw with page 0; this fills in the real page number. */\nfunction withPage(error: unknown, page: number): unknown {\n\tif (error instanceof PaginationError && error.page === 0) {\n\t\treturn new PaginationError(error.code, page, error.message, { cause: error });\n\t}\n\treturn error;\n}\n\nasync function* iterate<T>(\n\tfetchPage: FetchPage<T>,\n\tlimits: Limits,\n\tuserSignal: AbortSignal | undefined,\n): AsyncGenerator<T, void, undefined> {\n\tconst run = new AbortController();\n\tconst inflight: { ctl?: AbortController } = {};\n\tconst seen = new Set<string>();\n\tlet pageNo = 1;\n\tlet cursor: string | undefined;\n\tlet emitted = 0;\n\tlet emptyStreak = 0;\n\tlet totalTimer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst onUserAbort = (): void => {\n\t\trun.abort(\n\t\t\tnew PaginationError(\"ABORTED\", pageNo, \"Pagination was aborted\", {\n\t\t\t\tcause: userSignal?.reason,\n\t\t\t}),\n\t\t);\n\t};\n\n\t/**\n\t * Requests one page. The page signal is aborted when the run is aborted (user signal, total\n\t * timeout or iterator return) and after `timeoutMs`. The promise rejects as soon as the signal\n\t * fires, even if `fetchPage` ignores the signal.\n\t */\n\tconst request = (pageCursor: string | undefined, page: number): Promise<Page<T>> =>\n\t\tnew Promise<Page<T>>((resolve, reject) => {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\treject(run.signal.reason);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst ctl = new AbortController();\n\t\t\tinflight.ctl = ctl;\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst onParentAbort = (): void => ctl.abort(run.signal.reason);\n\t\t\tconst onPageAbort = (): void => settle(() => reject(ctl.signal.reason));\n\t\t\tconst settle = (done: () => void): void => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\trun.signal.removeEventListener(\"abort\", onParentAbort);\n\t\t\t\tctl.signal.removeEventListener(\"abort\", onPageAbort);\n\t\t\t\tdone();\n\t\t\t};\n\n\t\t\trun.signal.addEventListener(\"abort\", onParentAbort, { once: true });\n\t\t\tctl.signal.addEventListener(\"abort\", onPageAbort, { once: true });\n\t\t\tif (Number.isFinite(limits.timeoutMs)) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tctl.abort(\n\t\t\t\t\t\tnew PaginationError(\n\t\t\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\t\t\tpage,\n\t\t\t\t\t\t\t`Page ${page} timed out after ${limits.timeoutMs} ms`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}, limits.timeoutMs);\n\t\t\t}\n\n\t\t\tlet work: Promise<Page<T>>;\n\t\t\ttry {\n\t\t\t\twork = Promise.resolve(fetchPage(pageCursor, ctl.signal));\n\t\t\t} catch (error) {\n\t\t\t\twork = Promise.reject(error);\n\t\t\t}\n\t\t\twork.then(\n\t\t\t\t(result) => settle(() => resolve(result)),\n\t\t\t\t(error: unknown) => settle(() => reject(withPage(error, page))),\n\t\t\t);\n\t\t});\n\n\tif (userSignal?.aborted) {\n\t\tonUserAbort();\n\t} else {\n\t\tuserSignal?.addEventListener(\"abort\", onUserAbort, { once: true });\n\t}\n\tif (Number.isFinite(limits.totalTimeoutMs)) {\n\t\ttotalTimer = setTimeout(() => {\n\t\t\trun.abort(\n\t\t\t\tnew PaginationError(\n\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Total timeout of ${limits.totalTimeoutMs} ms exceeded`,\n\t\t\t\t),\n\t\t\t);\n\t\t}, limits.totalTimeoutMs);\n\t}\n\n\ttry {\n\t\twhile (emitted < limits.maxItems) {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\tthrow run.signal.reason;\n\t\t\t}\n\n\t\t\tconst result = await request(cursor, pageNo);\n\t\t\tif (!Array.isArray(result.items)) {\n\t\t\t\tthrow new TypeError(\"fetchPage must resolve to an object with an items array\");\n\t\t\t}\n\t\t\tconst next = normalizeCursor(result.nextCursor);\n\n\t\t\tif (next !== undefined && seen.has(next)) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"CURSOR_LOOP\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Server returned a cursor that was already used (page ${pageNo}); stopping`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (result.items.length === 0) {\n\t\t\t\tif (next !== undefined) {\n\t\t\t\t\temptyStreak += 1;\n\t\t\t\t\tif (emptyStreak >= MAX_EMPTY_PAGES) {\n\t\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\t\"EMPTY_PAGES_LOOP\",\n\t\t\t\t\t\t\tpageNo,\n\t\t\t\t\t\t\t`${MAX_EMPTY_PAGES} consecutive empty pages with a next cursor (page ${pageNo}); stopping`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\temptyStreak = 0;\n\t\t\t}\n\n\t\t\tfor (const item of result.items) {\n\t\t\t\tif (run.signal.aborted) {\n\t\t\t\t\tthrow run.signal.reason;\n\t\t\t\t}\n\t\t\t\tyield item;\n\t\t\t\temitted += 1;\n\t\t\t\tif (emitted >= limits.maxItems) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (next === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (pageNo >= limits.maxPages) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"PAGE_LIMIT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Page limit of ${limits.maxPages} reached while more pages are available`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tseen.add(next);\n\t\t\tcursor = next;\n\t\t\tpageNo += 1;\n\t\t}\n\t} finally {\n\t\tclearTimeout(totalTimer);\n\t\tuserSignal?.removeEventListener(\"abort\", onUserAbort);\n\t\t// Cancels the open request when the consumer stops early (break, return) or on errors.\n\t\tinflight.ctl?.abort();\n\t\trun.abort();\n\t}\n}\n\n/**\n * Iterates over all items of a paginated API.\n *\n * Guards: repeated cursors (`CURSOR_LOOP`), a page limit (`PAGE_LIMIT`), endless empty pages\n * (`EMPTY_PAGES_LOOP`), per-page and total timeouts (`TIMEOUT`) and an abort signal (`ABORTED`).\n * Reaching `maxItems` stops without error.\n *\n * Breaking out of a `for await` loop calls `return()`, which aborts the signal of any request\n * that is still open.\n *\n * Options are validated when `paginate()` is called. Iteration starts on the first `next()`.\n */\nexport function paginate<T>(options: PaginateOptions<T>): AsyncGenerator<T, void, undefined> {\n\tif (typeof options.fetchPage !== \"function\") {\n\t\tthrow new TypeError(\"fetchPage must be a function\");\n\t}\n\treturn iterate(options.fetchPage, resolveLimits(options), options.signal);\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -254,7 +254,13 @@ function createFetchPager(options) {
254
254
  continue;
255
255
  }
256
256
  if (!res.ok) throw new Error(`Request failed with HTTP ${res.status}`);
257
- const body = await res.json();
257
+ let body;
258
+ try {
259
+ body = await res.json();
260
+ } catch (error) {
261
+ if (error instanceof SyntaxError) throw new Error(`Response of HTTP ${res.status} is not valid JSON`, { cause: error });
262
+ throw error;
263
+ }
258
264
  const items = extract(body);
259
265
  if (!Array.isArray(items)) throw new TypeError("extract() must return an array of items");
260
266
  const raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/link-header.ts","../src/mask-url.ts","../src/fetch-pager.ts","../src/paginate.ts"],"sourcesContent":["export type PaginationErrorCode =\n\t| \"CURSOR_LOOP\"\n\t| \"PAGE_LIMIT\"\n\t| \"RATE_LIMITED\"\n\t| \"TIMEOUT\"\n\t| \"ABORTED\"\n\t| \"EMPTY_PAGES_LOOP\"\n\t| \"INVALID_LINK\";\n\n/**\n * Error raised by the pagination guards.\n *\n * `page` is the 1-based number of the page whose request or response caused the error.\n * Adapters such as `createFetchPager` do not know the page number and use `0`; `paginate()`\n * replaces it with the real number before the error reaches the caller.\n */\nexport class PaginationError extends Error {\n\treadonly code: PaginationErrorCode;\n\treadonly page: number;\n\n\tconstructor(\n\t\tcode: PaginationErrorCode,\n\t\tpage: number,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.name = \"PaginationError\";\n\t\tthis.code = code;\n\t\tthis.page = page;\n\t}\n}\n","import { PaginationError } from \"./errors.js\";\n\ninterface LinkScan {\n\tlinks: Record<string, string>;\n\t/** True when a malformed link-value mentions `rel=\"next\"`. */\n\tmalformedNext: boolean;\n}\n\nconst TOKEN_CHAR = /[!#$%&'*+\\-.^_`|~0-9A-Za-z]/;\nconst NEXT_MENTION = /\\brel\\s*=\\s*\"?[^\";,]*\\bnext\\b/i;\n\nfunction isTokenChar(ch: string): boolean {\n\treturn TOKEN_CHAR.test(ch);\n}\n\n/**\n * Tokenizer for RFC 8288 Link headers. URI references are read between `<` and `>`, so commas and\n * semicolons inside a URL are not treated as separators. Quoted parameter values support backslash\n * escapes. Malformed link-values are skipped; the scan resumes at the next `<`.\n */\nfunction scanLinkHeader(value: string): LinkScan {\n\tconst links: Record<string, string> = {};\n\tlet malformedNext = false;\n\tconst n = value.length;\n\tlet i = 0;\n\n\tconst skipSpaces = (): void => {\n\t\twhile (i < n && (value.charAt(i) === \" \" || value.charAt(i) === \"\\t\")) {\n\t\t\ti += 1;\n\t\t}\n\t};\n\n\twhile (i < n) {\n\t\tskipSpaces();\n\t\tif (i >= n) {\n\t\t\tbreak;\n\t\t}\n\t\tif (value.charAt(i) === \",\") {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst linkStart = i;\n\t\tlet target: string | undefined;\n\t\tif (value.charAt(i) === \"<\") {\n\t\t\tconst close = value.indexOf(\">\", i + 1);\n\t\t\tif (close !== -1) {\n\t\t\t\ttarget = value.slice(i + 1, close).trim();\n\t\t\t\ti = close + 1;\n\t\t\t}\n\t\t}\n\n\t\tlet ok = target !== undefined;\n\t\tconst rels: string[] = [];\n\n\t\twhile (ok) {\n\t\t\tskipSpaces();\n\t\t\tif (i >= n) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst ch = value.charAt(i);\n\t\t\tif (ch === \",\") {\n\t\t\t\ti += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (ch !== \";\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tskipSpaces();\n\n\t\t\tconst nameStart = i;\n\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tconst name = value.slice(nameStart, i).toLowerCase();\n\t\t\tif (name === \"\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tskipSpaces();\n\n\t\t\tlet paramValue: string | undefined;\n\t\t\tif (value.charAt(i) === \"=\") {\n\t\t\t\ti += 1;\n\t\t\t\tskipSpaces();\n\t\t\t\tif (value.charAt(i) === '\"') {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tlet quoted = \"\";\n\t\t\t\t\tlet closed = false;\n\t\t\t\t\twhile (i < n) {\n\t\t\t\t\t\tconst c = value.charAt(i);\n\t\t\t\t\t\tif (c === \"\\\\\" && i + 1 < n) {\n\t\t\t\t\t\t\tquoted += value.charAt(i + 1);\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t\tif (c === '\"') {\n\t\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tquoted += c;\n\t\t\t\t\t}\n\t\t\t\t\tif (!closed) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = quoted;\n\t\t\t\t} else {\n\t\t\t\t\tconst valueStart = i;\n\t\t\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t}\n\t\t\t\t\tif (i === valueStart) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = value.slice(valueStart, i);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (name === \"rel\" && paramValue !== undefined) {\n\t\t\t\trels.push(...paramValue.split(/\\s+/).filter((rel) => rel !== \"\"));\n\t\t\t}\n\t\t}\n\n\t\tif (ok && target !== undefined) {\n\t\t\tfor (const rel of rels) {\n\t\t\t\tconst key = rel.toLowerCase();\n\t\t\t\tif (key !== \"__proto__\" && !Object.hasOwn(links, key)) {\n\t\t\t\t\tlinks[key] = target;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmalformedNext = malformedNext || NEXT_MENTION.test(value.slice(linkStart));\n\t\t\tconst nextLink = value.indexOf(\"<\", linkStart + 1);\n\t\t\ti = nextLink === -1 ? n : nextLink;\n\t\t}\n\t}\n\n\treturn { links, malformedNext };\n}\n\n/**\n * Parses an RFC 8288 `Link` header value into a map from relation type to URI reference.\n *\n * Handles quoted and unquoted `rel` values, several relation types in one `rel`, several links\n * separated by commas, additional parameters and URIs that contain commas or semicolons.\n * Relation names are lower-cased. For duplicate relations the first link wins. Malformed\n * link-values are ignored.\n */\nexport function parseLinkHeader(value: string): Record<string, string> {\n\treturn scanLinkHeader(value).links;\n}\n\n/**\n * Returns the URI reference of the `rel=\"next\"` link from a `Link` header, or `undefined`.\n * The URI may be relative; resolve it against the request URL.\n *\n * Throws a `PaginationError` with code `INVALID_LINK` when a malformed link-value mentions\n * `rel=\"next\"` and no valid next link was found. Otherwise a broken header would look like the\n * last page and silently truncate the data.\n */\nexport function nextFromLinkHeader(headers: {\n\tget(name: string): string | null;\n}): string | undefined {\n\tconst raw = headers.get(\"link\");\n\tif (raw === null) {\n\t\treturn undefined;\n\t}\n\tconst { links, malformedNext } = scanLinkHeader(raw);\n\tif (Object.hasOwn(links, \"next\")) {\n\t\treturn links.next;\n\t}\n\tif (malformedNext) {\n\t\tthrow new PaginationError(\n\t\t\t\"INVALID_LINK\",\n\t\t\t0,\n\t\t\t'Malformed Link header: rel=\"next\" could not be parsed',\n\t\t);\n\t}\n\treturn undefined;\n}\n","/**\n * Returns a URL that is safe to put into an error message: credentials are removed, query values\n * are replaced with `***` (keys are kept) and fragments are dropped.\n */\nexport function maskUrl(input: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(input);\n\t} catch {\n\t\treturn \"<invalid URL>\";\n\t}\n\tconst origin = url.origin === \"null\" ? url.protocol : url.origin;\n\tconst keys = [...new Set(url.searchParams.keys())];\n\tconst query =\n\t\tkeys.length > 0 ? `?${keys.map((key) => `${encodeURIComponent(key)}=***`).join(\"&\")}` : \"\";\n\treturn `${origin}${url.pathname}${query}`;\n}\n","import { PaginationError } from \"./errors.js\";\nimport { nextFromLinkHeader } from \"./link-header.js\";\nimport { maskUrl } from \"./mask-url.js\";\nimport type { FetchPage } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst DEFAULT_RETRY_AFTER_MS = 1_000;\nconst MAX_RETRY_AFTER_MS = 60_000;\n\nexport interface FetchPagerOptions<T> {\n\t/** Fetch implementation to use. Defaults to `globalThis.fetch`. */\n\tfetch?: typeof fetch;\n\t/** URL of the first page. The cursor of every later page is a full URL. */\n\turl: string | URL;\n\t/** Maps the parsed JSON body to the items of the page. */\n\textract: (body: unknown) => T[];\n\t/**\n\t * Returns the URL of the next page, or `null` when there is none. Relative URLs are resolved\n\t * against the request URL. Defaults to the `rel=\"next\"` link of the `Link` header.\n\t */\n\tnextFromResponse?: (res: Response, body: unknown) => string | null | undefined;\n\t/**\n\t * Follow next URLs on a different origin than `url`. Defaults to `false`. Keep it `false`:\n\t * a compromised server could otherwise redirect the client to a foreign host and receive\n\t * credentials that the injected `fetch` attaches.\n\t */\n\tallowCrossOriginNext?: boolean;\n}\n\n/**\n * Parses the `Retry-After` value (delay in seconds or HTTP date) into milliseconds, capped at\n * 60 seconds. A missing or unparsable value falls back to one second.\n */\nfunction retryAfterMs(header: string | null, now: number): number {\n\tif (header === null) {\n\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t}\n\tconst value = header.trim();\n\tlet ms: number;\n\tif (/^\\d+$/.test(value)) {\n\t\tms = Number(value) * 1000;\n\t} else {\n\t\tconst date = Date.parse(value);\n\t\tif (Number.isNaN(date)) {\n\t\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t\t}\n\t\tms = Math.max(0, date - now);\n\t}\n\treturn Math.min(ms, MAX_RETRY_AFTER_MS);\n}\n\n/** Waits for `ms`, rejecting with the signal's reason if the signal aborts first. */\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tif (signal.aborted) {\n\t\t\treject(signal.reason);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => {\n\t\t\tclearTimeout(timer);\n\t\t\treject(signal.reason);\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Creates a `fetchPage` function for `paginate()` that follows URL-based cursors.\n *\n * - Next URLs come from the `Link` header (RFC 8288) or from `nextFromResponse`.\n * - Next URLs on a different origin than `url` are rejected with `INVALID_LINK`, unless\n * `allowCrossOriginNext` is set.\n * - HTTP 429 waits for `Retry-After` (seconds or HTTP date, capped at 60 s) and retries the same\n * page up to 3 times, then throws `RATE_LIMITED`. The wait counts towards the page timeout.\n * - Other non-2xx responses throw a plain `Error` that contains only the status code.\n *\n * Errors thrown here have `page: 0`; `paginate()` sets the real page number.\n */\nexport function createFetchPager<T>(options: FetchPagerOptions<T>): FetchPage<T> {\n\tconst doFetch = options.fetch ?? globalThis.fetch;\n\tif (typeof doFetch !== \"function\") {\n\t\tthrow new TypeError(\"No fetch implementation available; pass options.fetch\");\n\t}\n\tconst startUrl = new URL(options.url);\n\tconst startOrigin = startUrl.origin;\n\tconst allowCrossOrigin = options.allowCrossOriginNext === true;\n\tconst extract = options.extract;\n\tconst nextFromResponse = options.nextFromResponse;\n\n\tconst resolveNext = (raw: string | null | undefined, base: string): string | null => {\n\t\tif (raw === null || raw === undefined || raw === \"\") {\n\t\t\treturn null;\n\t\t}\n\t\tlet next: URL;\n\t\ttry {\n\t\t\tnext = new URL(raw, base);\n\t\t} catch {\n\t\t\tthrow new PaginationError(\"INVALID_LINK\", 0, \"Next link is not a valid URL\");\n\t\t}\n\t\tif (!allowCrossOrigin && next.origin !== startOrigin) {\n\t\t\tthrow new PaginationError(\n\t\t\t\t\"INVALID_LINK\",\n\t\t\t\t0,\n\t\t\t\t`Next link points to another origin (${next.origin}) and was not followed. ` +\n\t\t\t\t\t`Set allowCrossOriginNext to allow it. Link: ${maskUrl(next.href)}`,\n\t\t\t);\n\t\t}\n\t\treturn next.href;\n\t};\n\n\treturn async (cursor, signal) => {\n\t\tconst target = cursor === undefined ? startUrl.href : cursor;\n\t\tlet retries = 0;\n\n\t\tfor (;;) {\n\t\t\tconst res = await doFetch(target, { signal });\n\n\t\t\tif (res.status === 429) {\n\t\t\t\tawait res.body?.cancel().catch(() => undefined);\n\t\t\t\tif (retries >= MAX_RETRIES) {\n\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\"RATE_LIMITED\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t`Still rate limited (HTTP 429) after ${MAX_RETRIES} retries`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tretries += 1;\n\t\t\t\tawait sleep(retryAfterMs(res.headers.get(\"retry-after\"), Date.now()), signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`Request failed with HTTP ${res.status}`);\n\t\t\t}\n\n\t\t\tconst body: unknown = await res.json();\n\t\t\tconst items = extract(body);\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\tthrow new TypeError(\"extract() must return an array of items\");\n\t\t\t}\n\t\t\tconst raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);\n\t\t\treturn { items, nextCursor: resolveNext(raw, target) };\n\t\t}\n\t};\n}\n","import { PaginationError } from \"./errors.js\";\nimport type { FetchPage, Page, PaginateOptions } from \"./types.js\";\n\nconst DEFAULT_MAX_PAGES = 100;\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_EMPTY_PAGES = 3;\n\ninterface Limits {\n\tmaxPages: number;\n\tmaxItems: number;\n\ttimeoutMs: number;\n\ttotalTimeoutMs: number;\n}\n\nfunction resolveLimits<T>(options: PaginateOptions<T>): Limits {\n\tconst maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;\n\tconst maxItems = options.maxItems ?? Number.POSITIVE_INFINITY;\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst totalTimeoutMs = options.totalTimeoutMs ?? Number.POSITIVE_INFINITY;\n\n\tif (!(maxPages === Number.POSITIVE_INFINITY || (Number.isInteger(maxPages) && maxPages >= 1))) {\n\t\tthrow new RangeError(\"maxPages must be a positive integer or Infinity\");\n\t}\n\tif (!(maxItems === Number.POSITIVE_INFINITY || (Number.isInteger(maxItems) && maxItems >= 0))) {\n\t\tthrow new RangeError(\"maxItems must be a non-negative integer or Infinity\");\n\t}\n\tif (!(timeoutMs > 0)) {\n\t\tthrow new RangeError(\"timeoutMs must be greater than 0\");\n\t}\n\tif (!(totalTimeoutMs > 0)) {\n\t\tthrow new RangeError(\"totalTimeoutMs must be greater than 0\");\n\t}\n\treturn { maxPages, maxItems, timeoutMs, totalTimeoutMs };\n}\n\n/** `undefined`, `null` and `\"\"` mean \"no next page\". */\nfunction normalizeCursor(value: string | null | undefined): string | undefined {\n\treturn value === undefined || value === null || value === \"\" ? undefined : value;\n}\n\n/** Adapters without page context throw with page 0; this fills in the real page number. */\nfunction withPage(error: unknown, page: number): unknown {\n\tif (error instanceof PaginationError && error.page === 0) {\n\t\treturn new PaginationError(error.code, page, error.message, { cause: error });\n\t}\n\treturn error;\n}\n\nasync function* iterate<T>(\n\tfetchPage: FetchPage<T>,\n\tlimits: Limits,\n\tuserSignal: AbortSignal | undefined,\n): AsyncGenerator<T, void, undefined> {\n\tconst run = new AbortController();\n\tconst inflight: { ctl?: AbortController } = {};\n\tconst seen = new Set<string>();\n\tlet pageNo = 1;\n\tlet cursor: string | undefined;\n\tlet emitted = 0;\n\tlet emptyStreak = 0;\n\tlet totalTimer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst onUserAbort = (): void => {\n\t\trun.abort(\n\t\t\tnew PaginationError(\"ABORTED\", pageNo, \"Pagination was aborted\", {\n\t\t\t\tcause: userSignal?.reason,\n\t\t\t}),\n\t\t);\n\t};\n\n\t/**\n\t * Requests one page. The page signal is aborted when the run is aborted (user signal, total\n\t * timeout or iterator return) and after `timeoutMs`. The promise rejects as soon as the signal\n\t * fires, even if `fetchPage` ignores the signal.\n\t */\n\tconst request = (pageCursor: string | undefined, page: number): Promise<Page<T>> =>\n\t\tnew Promise<Page<T>>((resolve, reject) => {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\treject(run.signal.reason);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst ctl = new AbortController();\n\t\t\tinflight.ctl = ctl;\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst onParentAbort = (): void => ctl.abort(run.signal.reason);\n\t\t\tconst onPageAbort = (): void => settle(() => reject(ctl.signal.reason));\n\t\t\tconst settle = (done: () => void): void => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\trun.signal.removeEventListener(\"abort\", onParentAbort);\n\t\t\t\tctl.signal.removeEventListener(\"abort\", onPageAbort);\n\t\t\t\tdone();\n\t\t\t};\n\n\t\t\trun.signal.addEventListener(\"abort\", onParentAbort, { once: true });\n\t\t\tctl.signal.addEventListener(\"abort\", onPageAbort, { once: true });\n\t\t\tif (Number.isFinite(limits.timeoutMs)) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tctl.abort(\n\t\t\t\t\t\tnew PaginationError(\n\t\t\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\t\t\tpage,\n\t\t\t\t\t\t\t`Page ${page} timed out after ${limits.timeoutMs} ms`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}, limits.timeoutMs);\n\t\t\t}\n\n\t\t\tlet work: Promise<Page<T>>;\n\t\t\ttry {\n\t\t\t\twork = Promise.resolve(fetchPage(pageCursor, ctl.signal));\n\t\t\t} catch (error) {\n\t\t\t\twork = Promise.reject(error);\n\t\t\t}\n\t\t\twork.then(\n\t\t\t\t(result) => settle(() => resolve(result)),\n\t\t\t\t(error: unknown) => settle(() => reject(withPage(error, page))),\n\t\t\t);\n\t\t});\n\n\tif (userSignal?.aborted) {\n\t\tonUserAbort();\n\t} else {\n\t\tuserSignal?.addEventListener(\"abort\", onUserAbort, { once: true });\n\t}\n\tif (Number.isFinite(limits.totalTimeoutMs)) {\n\t\ttotalTimer = setTimeout(() => {\n\t\t\trun.abort(\n\t\t\t\tnew PaginationError(\n\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Total timeout of ${limits.totalTimeoutMs} ms exceeded`,\n\t\t\t\t),\n\t\t\t);\n\t\t}, limits.totalTimeoutMs);\n\t}\n\n\ttry {\n\t\twhile (emitted < limits.maxItems) {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\tthrow run.signal.reason;\n\t\t\t}\n\n\t\t\tconst result = await request(cursor, pageNo);\n\t\t\tif (!Array.isArray(result.items)) {\n\t\t\t\tthrow new TypeError(\"fetchPage must resolve to an object with an items array\");\n\t\t\t}\n\t\t\tconst next = normalizeCursor(result.nextCursor);\n\n\t\t\tif (next !== undefined && seen.has(next)) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"CURSOR_LOOP\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Server returned a cursor that was already used (page ${pageNo}); stopping`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (result.items.length === 0) {\n\t\t\t\tif (next !== undefined) {\n\t\t\t\t\temptyStreak += 1;\n\t\t\t\t\tif (emptyStreak >= MAX_EMPTY_PAGES) {\n\t\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\t\"EMPTY_PAGES_LOOP\",\n\t\t\t\t\t\t\tpageNo,\n\t\t\t\t\t\t\t`${MAX_EMPTY_PAGES} consecutive empty pages with a next cursor (page ${pageNo}); stopping`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\temptyStreak = 0;\n\t\t\t}\n\n\t\t\tfor (const item of result.items) {\n\t\t\t\tif (run.signal.aborted) {\n\t\t\t\t\tthrow run.signal.reason;\n\t\t\t\t}\n\t\t\t\tyield item;\n\t\t\t\temitted += 1;\n\t\t\t\tif (emitted >= limits.maxItems) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (next === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (pageNo >= limits.maxPages) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"PAGE_LIMIT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Page limit of ${limits.maxPages} reached while more pages are available`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tseen.add(next);\n\t\t\tcursor = next;\n\t\t\tpageNo += 1;\n\t\t}\n\t} finally {\n\t\tclearTimeout(totalTimer);\n\t\tuserSignal?.removeEventListener(\"abort\", onUserAbort);\n\t\t// Cancels the open request when the consumer stops early (break, return) or on errors.\n\t\tinflight.ctl?.abort();\n\t\trun.abort();\n\t}\n}\n\n/**\n * Iterates over all items of a paginated API.\n *\n * Guards: repeated cursors (`CURSOR_LOOP`), a page limit (`PAGE_LIMIT`), endless empty pages\n * (`EMPTY_PAGES_LOOP`), per-page and total timeouts (`TIMEOUT`) and an abort signal (`ABORTED`).\n * Reaching `maxItems` stops without error.\n *\n * Breaking out of a `for await` loop calls `return()`, which aborts the signal of any request\n * that is still open.\n *\n * Options are validated when `paginate()` is called. Iteration starts on the first `next()`.\n */\nexport function paginate<T>(options: PaginateOptions<T>): AsyncGenerator<T, void, undefined> {\n\tif (typeof options.fetchPage !== \"function\") {\n\t\tthrow new TypeError(\"fetchPage must be a function\");\n\t}\n\treturn iterate(options.fetchPage, resolveLimits(options), options.signal);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/link-header.ts","../src/mask-url.ts","../src/fetch-pager.ts","../src/paginate.ts"],"sourcesContent":["export type PaginationErrorCode =\n\t| \"CURSOR_LOOP\"\n\t| \"PAGE_LIMIT\"\n\t| \"RATE_LIMITED\"\n\t| \"TIMEOUT\"\n\t| \"ABORTED\"\n\t| \"EMPTY_PAGES_LOOP\"\n\t| \"INVALID_LINK\";\n\n/**\n * Error raised by the pagination guards.\n *\n * `page` is the 1-based number of the page whose request or response caused the error.\n * Adapters such as `createFetchPager` do not know the page number and use `0`; `paginate()`\n * replaces it with the real number before the error reaches the caller.\n */\nexport class PaginationError extends Error {\n\treadonly code: PaginationErrorCode;\n\treadonly page: number;\n\n\tconstructor(\n\t\tcode: PaginationErrorCode,\n\t\tpage: number,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.name = \"PaginationError\";\n\t\tthis.code = code;\n\t\tthis.page = page;\n\t}\n}\n","import { PaginationError } from \"./errors.js\";\n\ninterface LinkScan {\n\tlinks: Record<string, string>;\n\t/** True when a malformed link-value mentions `rel=\"next\"`. */\n\tmalformedNext: boolean;\n}\n\nconst TOKEN_CHAR = /[!#$%&'*+\\-.^_`|~0-9A-Za-z]/;\nconst NEXT_MENTION = /\\brel\\s*=\\s*\"?[^\";,]*\\bnext\\b/i;\n\nfunction isTokenChar(ch: string): boolean {\n\treturn TOKEN_CHAR.test(ch);\n}\n\n/**\n * Tokenizer for RFC 8288 Link headers. URI references are read between `<` and `>`, so commas and\n * semicolons inside a URL are not treated as separators. Quoted parameter values support backslash\n * escapes. Malformed link-values are skipped; the scan resumes at the next `<`.\n */\nfunction scanLinkHeader(value: string): LinkScan {\n\tconst links: Record<string, string> = {};\n\tlet malformedNext = false;\n\tconst n = value.length;\n\tlet i = 0;\n\n\tconst skipSpaces = (): void => {\n\t\twhile (i < n && (value.charAt(i) === \" \" || value.charAt(i) === \"\\t\")) {\n\t\t\ti += 1;\n\t\t}\n\t};\n\n\twhile (i < n) {\n\t\tskipSpaces();\n\t\tif (i >= n) {\n\t\t\tbreak;\n\t\t}\n\t\tif (value.charAt(i) === \",\") {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst linkStart = i;\n\t\tlet target: string | undefined;\n\t\tif (value.charAt(i) === \"<\") {\n\t\t\tconst close = value.indexOf(\">\", i + 1);\n\t\t\tif (close !== -1) {\n\t\t\t\ttarget = value.slice(i + 1, close).trim();\n\t\t\t\ti = close + 1;\n\t\t\t}\n\t\t}\n\n\t\tlet ok = target !== undefined;\n\t\tconst rels: string[] = [];\n\n\t\twhile (ok) {\n\t\t\tskipSpaces();\n\t\t\tif (i >= n) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst ch = value.charAt(i);\n\t\t\tif (ch === \",\") {\n\t\t\t\ti += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (ch !== \";\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tskipSpaces();\n\n\t\t\tconst nameStart = i;\n\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tconst name = value.slice(nameStart, i).toLowerCase();\n\t\t\tif (name === \"\") {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tskipSpaces();\n\n\t\t\tlet paramValue: string | undefined;\n\t\t\tif (value.charAt(i) === \"=\") {\n\t\t\t\ti += 1;\n\t\t\t\tskipSpaces();\n\t\t\t\tif (value.charAt(i) === '\"') {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tlet quoted = \"\";\n\t\t\t\t\tlet closed = false;\n\t\t\t\t\twhile (i < n) {\n\t\t\t\t\t\tconst c = value.charAt(i);\n\t\t\t\t\t\tif (c === \"\\\\\" && i + 1 < n) {\n\t\t\t\t\t\t\tquoted += value.charAt(i + 1);\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t\tif (c === '\"') {\n\t\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tquoted += c;\n\t\t\t\t\t}\n\t\t\t\t\tif (!closed) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = quoted;\n\t\t\t\t} else {\n\t\t\t\t\tconst valueStart = i;\n\t\t\t\t\twhile (i < n && isTokenChar(value.charAt(i))) {\n\t\t\t\t\t\ti += 1;\n\t\t\t\t\t}\n\t\t\t\t\tif (i === valueStart) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tparamValue = value.slice(valueStart, i);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (name === \"rel\" && paramValue !== undefined) {\n\t\t\t\trels.push(...paramValue.split(/\\s+/).filter((rel) => rel !== \"\"));\n\t\t\t}\n\t\t}\n\n\t\tif (ok && target !== undefined) {\n\t\t\tfor (const rel of rels) {\n\t\t\t\tconst key = rel.toLowerCase();\n\t\t\t\tif (key !== \"__proto__\" && !Object.hasOwn(links, key)) {\n\t\t\t\t\tlinks[key] = target;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmalformedNext = malformedNext || NEXT_MENTION.test(value.slice(linkStart));\n\t\t\tconst nextLink = value.indexOf(\"<\", linkStart + 1);\n\t\t\ti = nextLink === -1 ? n : nextLink;\n\t\t}\n\t}\n\n\treturn { links, malformedNext };\n}\n\n/**\n * Parses an RFC 8288 `Link` header value into a map from relation type to URI reference.\n *\n * Handles quoted and unquoted `rel` values, several relation types in one `rel`, several links\n * separated by commas, additional parameters and URIs that contain commas or semicolons.\n * Relation names are lower-cased. For duplicate relations the first link wins. Malformed\n * link-values are ignored.\n */\nexport function parseLinkHeader(value: string): Record<string, string> {\n\treturn scanLinkHeader(value).links;\n}\n\n/**\n * Returns the URI reference of the `rel=\"next\"` link from a `Link` header, or `undefined`.\n * The URI may be relative; resolve it against the request URL.\n *\n * Throws a `PaginationError` with code `INVALID_LINK` when a malformed link-value mentions\n * `rel=\"next\"` and no valid next link was found. Otherwise a broken header would look like the\n * last page and silently truncate the data.\n */\nexport function nextFromLinkHeader(headers: {\n\tget(name: string): string | null;\n}): string | undefined {\n\tconst raw = headers.get(\"link\");\n\tif (raw === null) {\n\t\treturn undefined;\n\t}\n\tconst { links, malformedNext } = scanLinkHeader(raw);\n\tif (Object.hasOwn(links, \"next\")) {\n\t\treturn links.next;\n\t}\n\tif (malformedNext) {\n\t\tthrow new PaginationError(\n\t\t\t\"INVALID_LINK\",\n\t\t\t0,\n\t\t\t'Malformed Link header: rel=\"next\" could not be parsed',\n\t\t);\n\t}\n\treturn undefined;\n}\n","/**\n * Returns a URL that is safe to put into an error message: credentials are removed, query values\n * are replaced with `***` (keys are kept) and fragments are dropped.\n */\nexport function maskUrl(input: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(input);\n\t} catch {\n\t\treturn \"<invalid URL>\";\n\t}\n\tconst origin = url.origin === \"null\" ? url.protocol : url.origin;\n\tconst keys = [...new Set(url.searchParams.keys())];\n\tconst query =\n\t\tkeys.length > 0 ? `?${keys.map((key) => `${encodeURIComponent(key)}=***`).join(\"&\")}` : \"\";\n\treturn `${origin}${url.pathname}${query}`;\n}\n","import { PaginationError } from \"./errors.js\";\nimport { nextFromLinkHeader } from \"./link-header.js\";\nimport { maskUrl } from \"./mask-url.js\";\nimport type { FetchPage } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst DEFAULT_RETRY_AFTER_MS = 1_000;\nconst MAX_RETRY_AFTER_MS = 60_000;\n\nexport interface FetchPagerOptions<T> {\n\t/** Fetch implementation to use. Defaults to `globalThis.fetch`. */\n\tfetch?: typeof fetch;\n\t/** URL of the first page. The cursor of every later page is a full URL. */\n\turl: string | URL;\n\t/** Maps the parsed JSON body to the items of the page. */\n\textract: (body: unknown) => T[];\n\t/**\n\t * Returns the URL of the next page, or `null` when there is none. Relative URLs are resolved\n\t * against the request URL. Defaults to the `rel=\"next\"` link of the `Link` header.\n\t */\n\tnextFromResponse?: (res: Response, body: unknown) => string | null | undefined;\n\t/**\n\t * Follow next URLs on a different origin than `url`. Defaults to `false`. Keep it `false`:\n\t * a compromised server could otherwise redirect the client to a foreign host and receive\n\t * credentials that the injected `fetch` attaches.\n\t */\n\tallowCrossOriginNext?: boolean;\n}\n\n/**\n * Parses the `Retry-After` value (delay in seconds or HTTP date) into milliseconds, capped at\n * 60 seconds. A missing or unparsable value falls back to one second.\n */\nfunction retryAfterMs(header: string | null, now: number): number {\n\tif (header === null) {\n\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t}\n\tconst value = header.trim();\n\tlet ms: number;\n\tif (/^\\d+$/.test(value)) {\n\t\tms = Number(value) * 1000;\n\t} else {\n\t\tconst date = Date.parse(value);\n\t\tif (Number.isNaN(date)) {\n\t\t\treturn DEFAULT_RETRY_AFTER_MS;\n\t\t}\n\t\tms = Math.max(0, date - now);\n\t}\n\treturn Math.min(ms, MAX_RETRY_AFTER_MS);\n}\n\n/** Waits for `ms`, rejecting with the signal's reason if the signal aborts first. */\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tif (signal.aborted) {\n\t\t\treject(signal.reason);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => {\n\t\t\tclearTimeout(timer);\n\t\t\treject(signal.reason);\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Creates a `fetchPage` function for `paginate()` that follows URL-based cursors.\n *\n * - Next URLs come from the `Link` header (RFC 8288) or from `nextFromResponse`.\n * - Next URLs on a different origin than `url` are rejected with `INVALID_LINK`, unless\n * `allowCrossOriginNext` is set.\n * - HTTP 429 waits for `Retry-After` (seconds or HTTP date, capped at 60 s) and retries the same\n * page up to 3 times, then throws `RATE_LIMITED`. The wait counts towards the page timeout.\n * - Other non-2xx responses throw a plain `Error` that contains only the status code.\n *\n * Errors thrown here have `page: 0`; `paginate()` sets the real page number.\n */\nexport function createFetchPager<T>(options: FetchPagerOptions<T>): FetchPage<T> {\n\tconst doFetch = options.fetch ?? globalThis.fetch;\n\tif (typeof doFetch !== \"function\") {\n\t\tthrow new TypeError(\"No fetch implementation available; pass options.fetch\");\n\t}\n\tconst startUrl = new URL(options.url);\n\tconst startOrigin = startUrl.origin;\n\tconst allowCrossOrigin = options.allowCrossOriginNext === true;\n\tconst extract = options.extract;\n\tconst nextFromResponse = options.nextFromResponse;\n\n\tconst resolveNext = (raw: string | null | undefined, base: string): string | null => {\n\t\tif (raw === null || raw === undefined || raw === \"\") {\n\t\t\treturn null;\n\t\t}\n\t\tlet next: URL;\n\t\ttry {\n\t\t\tnext = new URL(raw, base);\n\t\t} catch {\n\t\t\tthrow new PaginationError(\"INVALID_LINK\", 0, \"Next link is not a valid URL\");\n\t\t}\n\t\tif (!allowCrossOrigin && next.origin !== startOrigin) {\n\t\t\tthrow new PaginationError(\n\t\t\t\t\"INVALID_LINK\",\n\t\t\t\t0,\n\t\t\t\t`Next link points to another origin (${next.origin}) and was not followed. ` +\n\t\t\t\t\t`Set allowCrossOriginNext to allow it. Link: ${maskUrl(next.href)}`,\n\t\t\t);\n\t\t}\n\t\treturn next.href;\n\t};\n\n\treturn async (cursor, signal) => {\n\t\tconst target = cursor === undefined ? startUrl.href : cursor;\n\t\tlet retries = 0;\n\n\t\tfor (;;) {\n\t\t\tconst res = await doFetch(target, { signal });\n\n\t\t\tif (res.status === 429) {\n\t\t\t\tawait res.body?.cancel().catch(() => undefined);\n\t\t\t\tif (retries >= MAX_RETRIES) {\n\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\"RATE_LIMITED\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t`Still rate limited (HTTP 429) after ${MAX_RETRIES} retries`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tretries += 1;\n\t\t\t\tawait sleep(retryAfterMs(res.headers.get(\"retry-after\"), Date.now()), signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`Request failed with HTTP ${res.status}`);\n\t\t\t}\n\n\t\t\tlet body: unknown;\n\t\t\ttry {\n\t\t\t\tbody = await res.json();\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\t\t// Name the response without echoing its body, which may contain secrets.\n\t\t\t\t\tthrow new Error(`Response of HTTP ${res.status} is not valid JSON`, { cause: error });\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst items = extract(body);\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\tthrow new TypeError(\"extract() must return an array of items\");\n\t\t\t}\n\t\t\tconst raw = nextFromResponse ? nextFromResponse(res, body) : nextFromLinkHeader(res.headers);\n\t\t\treturn { items, nextCursor: resolveNext(raw, target) };\n\t\t}\n\t};\n}\n","import { PaginationError } from \"./errors.js\";\nimport type { FetchPage, Page, PaginateOptions } from \"./types.js\";\n\nconst DEFAULT_MAX_PAGES = 100;\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_EMPTY_PAGES = 3;\n\ninterface Limits {\n\tmaxPages: number;\n\tmaxItems: number;\n\ttimeoutMs: number;\n\ttotalTimeoutMs: number;\n}\n\nfunction resolveLimits<T>(options: PaginateOptions<T>): Limits {\n\tconst maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;\n\tconst maxItems = options.maxItems ?? Number.POSITIVE_INFINITY;\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst totalTimeoutMs = options.totalTimeoutMs ?? Number.POSITIVE_INFINITY;\n\n\tif (!(maxPages === Number.POSITIVE_INFINITY || (Number.isInteger(maxPages) && maxPages >= 1))) {\n\t\tthrow new RangeError(\"maxPages must be a positive integer or Infinity\");\n\t}\n\tif (!(maxItems === Number.POSITIVE_INFINITY || (Number.isInteger(maxItems) && maxItems >= 0))) {\n\t\tthrow new RangeError(\"maxItems must be a non-negative integer or Infinity\");\n\t}\n\tif (!(timeoutMs > 0)) {\n\t\tthrow new RangeError(\"timeoutMs must be greater than 0\");\n\t}\n\tif (!(totalTimeoutMs > 0)) {\n\t\tthrow new RangeError(\"totalTimeoutMs must be greater than 0\");\n\t}\n\treturn { maxPages, maxItems, timeoutMs, totalTimeoutMs };\n}\n\n/** `undefined`, `null` and `\"\"` mean \"no next page\". */\nfunction normalizeCursor(value: string | null | undefined): string | undefined {\n\treturn value === undefined || value === null || value === \"\" ? undefined : value;\n}\n\n/** Adapters without page context throw with page 0; this fills in the real page number. */\nfunction withPage(error: unknown, page: number): unknown {\n\tif (error instanceof PaginationError && error.page === 0) {\n\t\treturn new PaginationError(error.code, page, error.message, { cause: error });\n\t}\n\treturn error;\n}\n\nasync function* iterate<T>(\n\tfetchPage: FetchPage<T>,\n\tlimits: Limits,\n\tuserSignal: AbortSignal | undefined,\n): AsyncGenerator<T, void, undefined> {\n\tconst run = new AbortController();\n\tconst inflight: { ctl?: AbortController } = {};\n\tconst seen = new Set<string>();\n\tlet pageNo = 1;\n\tlet cursor: string | undefined;\n\tlet emitted = 0;\n\tlet emptyStreak = 0;\n\tlet totalTimer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst onUserAbort = (): void => {\n\t\trun.abort(\n\t\t\tnew PaginationError(\"ABORTED\", pageNo, \"Pagination was aborted\", {\n\t\t\t\tcause: userSignal?.reason,\n\t\t\t}),\n\t\t);\n\t};\n\n\t/**\n\t * Requests one page. The page signal is aborted when the run is aborted (user signal, total\n\t * timeout or iterator return) and after `timeoutMs`. The promise rejects as soon as the signal\n\t * fires, even if `fetchPage` ignores the signal.\n\t */\n\tconst request = (pageCursor: string | undefined, page: number): Promise<Page<T>> =>\n\t\tnew Promise<Page<T>>((resolve, reject) => {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\treject(run.signal.reason);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst ctl = new AbortController();\n\t\t\tinflight.ctl = ctl;\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst onParentAbort = (): void => ctl.abort(run.signal.reason);\n\t\t\tconst onPageAbort = (): void => settle(() => reject(ctl.signal.reason));\n\t\t\tconst settle = (done: () => void): void => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\trun.signal.removeEventListener(\"abort\", onParentAbort);\n\t\t\t\tctl.signal.removeEventListener(\"abort\", onPageAbort);\n\t\t\t\tdone();\n\t\t\t};\n\n\t\t\trun.signal.addEventListener(\"abort\", onParentAbort, { once: true });\n\t\t\tctl.signal.addEventListener(\"abort\", onPageAbort, { once: true });\n\t\t\tif (Number.isFinite(limits.timeoutMs)) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tctl.abort(\n\t\t\t\t\t\tnew PaginationError(\n\t\t\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\t\t\tpage,\n\t\t\t\t\t\t\t`Page ${page} timed out after ${limits.timeoutMs} ms`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}, limits.timeoutMs);\n\t\t\t}\n\n\t\t\tlet work: Promise<Page<T>>;\n\t\t\ttry {\n\t\t\t\twork = Promise.resolve(fetchPage(pageCursor, ctl.signal));\n\t\t\t} catch (error) {\n\t\t\t\twork = Promise.reject(error);\n\t\t\t}\n\t\t\twork.then(\n\t\t\t\t(result) => settle(() => resolve(result)),\n\t\t\t\t(error: unknown) => settle(() => reject(withPage(error, page))),\n\t\t\t);\n\t\t});\n\n\tif (userSignal?.aborted) {\n\t\tonUserAbort();\n\t} else {\n\t\tuserSignal?.addEventListener(\"abort\", onUserAbort, { once: true });\n\t}\n\tif (Number.isFinite(limits.totalTimeoutMs)) {\n\t\ttotalTimer = setTimeout(() => {\n\t\t\trun.abort(\n\t\t\t\tnew PaginationError(\n\t\t\t\t\t\"TIMEOUT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Total timeout of ${limits.totalTimeoutMs} ms exceeded`,\n\t\t\t\t),\n\t\t\t);\n\t\t}, limits.totalTimeoutMs);\n\t}\n\n\ttry {\n\t\twhile (emitted < limits.maxItems) {\n\t\t\tif (run.signal.aborted) {\n\t\t\t\tthrow run.signal.reason;\n\t\t\t}\n\n\t\t\tconst result = await request(cursor, pageNo);\n\t\t\tif (!Array.isArray(result.items)) {\n\t\t\t\tthrow new TypeError(\"fetchPage must resolve to an object with an items array\");\n\t\t\t}\n\t\t\tconst next = normalizeCursor(result.nextCursor);\n\n\t\t\tif (next !== undefined && seen.has(next)) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"CURSOR_LOOP\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Server returned a cursor that was already used (page ${pageNo}); stopping`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (result.items.length === 0) {\n\t\t\t\tif (next !== undefined) {\n\t\t\t\t\temptyStreak += 1;\n\t\t\t\t\tif (emptyStreak >= MAX_EMPTY_PAGES) {\n\t\t\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\t\t\"EMPTY_PAGES_LOOP\",\n\t\t\t\t\t\t\tpageNo,\n\t\t\t\t\t\t\t`${MAX_EMPTY_PAGES} consecutive empty pages with a next cursor (page ${pageNo}); stopping`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\temptyStreak = 0;\n\t\t\t}\n\n\t\t\tfor (const item of result.items) {\n\t\t\t\tif (run.signal.aborted) {\n\t\t\t\t\tthrow run.signal.reason;\n\t\t\t\t}\n\t\t\t\tyield item;\n\t\t\t\temitted += 1;\n\t\t\t\tif (emitted >= limits.maxItems) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (next === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (pageNo >= limits.maxPages) {\n\t\t\t\tthrow new PaginationError(\n\t\t\t\t\t\"PAGE_LIMIT\",\n\t\t\t\t\tpageNo,\n\t\t\t\t\t`Page limit of ${limits.maxPages} reached while more pages are available`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tseen.add(next);\n\t\t\tcursor = next;\n\t\t\tpageNo += 1;\n\t\t}\n\t} finally {\n\t\tclearTimeout(totalTimer);\n\t\tuserSignal?.removeEventListener(\"abort\", onUserAbort);\n\t\t// Cancels the open request when the consumer stops early (break, return) or on errors.\n\t\tinflight.ctl?.abort();\n\t\trun.abort();\n\t}\n}\n\n/**\n * Iterates over all items of a paginated API.\n *\n * Guards: repeated cursors (`CURSOR_LOOP`), a page limit (`PAGE_LIMIT`), endless empty pages\n * (`EMPTY_PAGES_LOOP`), per-page and total timeouts (`TIMEOUT`) and an abort signal (`ABORTED`).\n * Reaching `maxItems` stops without error.\n *\n * Breaking out of a `for await` loop calls `return()`, which aborts the signal of any request\n * that is still open.\n *\n * Options are validated when `paginate()` is called. Iteration starts on the first `next()`.\n */\nexport function paginate<T>(options: PaginateOptions<T>): AsyncGenerator<T, void, undefined> {\n\tif (typeof options.fetchPage !== \"function\") {\n\t\tthrow new TypeError(\"fetchPage must be a function\");\n\t}\n\treturn iterate(options.fetchPage, resolveLimits(options), options.signal);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,85 +1,83 @@
1
1
  {
2
- "name": "@yedoma-labs/ilyn-pages",
3
- "version": "1.0.0",
4
- "description": "Zero-dependency async iteration over cursor and Link-header paginated APIs with loop, page, rate-limit and timeout guards",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "import": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
14
- },
15
- "require": {
16
- "types": "./dist/index.d.cts",
17
- "default": "./dist/index.cjs"
18
- }
19
- }
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "engines": {
25
- "node": ">=20"
26
- },
27
- "packageManager": "pnpm@12.15.0",
28
- "scripts": {
29
- "build": "tsdown",
30
- "test": "vitest run",
31
- "test:watch": "vitest",
32
- "test:coverage": "vitest run --coverage",
33
- "lint": "biome check src tests",
34
- "lint:fix": "biome check --write src tests",
35
- "format": "biome format --write src tests",
36
- "typecheck": "tsc --noEmit",
37
- "prepublishOnly": "pnpm run build"
38
- },
39
- "keywords": [
40
- "pagination",
41
- "cursor",
42
- "link-header",
43
- "rfc8288",
44
- "async-iterator",
45
- "fetch",
46
- "rate-limit"
47
- ],
48
- "repository": {
49
- "type": "git",
50
- "url": "https://github.com/yedoma-labs/ilyn-pages"
51
- },
52
- "author": "",
53
- "license": "MIT",
54
- "simple-git-hooks": {
55
- "pre-commit": "pnpm exec lint-staged",
56
- "pre-push": "pnpm run typecheck && pnpm run test",
57
- "commit-msg": "pnpm exec commitlint --edit $1"
58
- },
59
- "lint-staged": {
60
- "*.ts": [
61
- "biome check --write"
62
- ]
63
- },
64
- "devDependencies": {
65
- "@biomejs/biome": "2.6.3",
66
- "@commitlint/cli": "^21.2.4",
67
- "@commitlint/config-conventional": "^21.2.4",
68
- "@semantic-release/changelog": "^7.0.0",
69
- "@semantic-release/commit-analyzer": "^13.0.1",
70
- "@semantic-release/exec": "^7.1.0",
71
- "@semantic-release/git": "^11.0.1",
72
- "@semantic-release/github": "^12.0.10",
73
- "@semantic-release/npm": "^13.2.0",
74
- "@semantic-release/release-notes-generator": "^14.1.1",
75
- "@types/node": "26.6.5",
76
- "@typescript/typescript6": "^6.0.2",
77
- "@vitest/coverage-v8": "^5.0.5",
78
- "lint-staged": "^17.6.0",
79
- "semantic-release": "^25.0.9",
80
- "simple-git-hooks": "^2.14.0",
81
- "tsdown": "^0.23.2",
82
- "typescript": "7.0.2",
83
- "vitest": "5.0.5"
84
- }
85
- }
2
+ "name": "@yedoma-labs/ilyn-pages",
3
+ "version": "1.0.1",
4
+ "description": "Zero-dependency async iteration over cursor and Link-header paginated APIs with loop, page, rate-limit and timeout guards",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "pagination",
29
+ "cursor",
30
+ "link-header",
31
+ "rfc8288",
32
+ "async-iterator",
33
+ "fetch",
34
+ "rate-limit"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/yedoma-labs/ilyn-pages"
39
+ },
40
+ "author": "",
41
+ "license": "MIT",
42
+ "simple-git-hooks": {
43
+ "pre-commit": "pnpm exec lint-staged",
44
+ "pre-push": "pnpm run typecheck && pnpm run test",
45
+ "commit-msg": "pnpm exec commitlint --edit $1"
46
+ },
47
+ "lint-staged": {
48
+ "*.ts": [
49
+ "biome check --write"
50
+ ]
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "2.6.3",
54
+ "@commitlint/cli": "^21.2.4",
55
+ "@commitlint/config-conventional": "^21.2.4",
56
+ "@semantic-release/changelog": "^7.0.0",
57
+ "@semantic-release/commit-analyzer": "^13.0.1",
58
+ "@semantic-release/exec": "^7.1.0",
59
+ "@semantic-release/git": "^11.0.1",
60
+ "@semantic-release/github": "^12.0.10",
61
+ "@semantic-release/npm": "^13.2.0",
62
+ "@semantic-release/release-notes-generator": "^14.1.1",
63
+ "@types/node": "26.6.5",
64
+ "@typescript/typescript6": "^6.0.2",
65
+ "@vitest/coverage-v8": "^5.0.5",
66
+ "lint-staged": "^17.6.0",
67
+ "semantic-release": "^25.0.9",
68
+ "simple-git-hooks": "^2.14.0",
69
+ "tsdown": "^0.23.2",
70
+ "typescript": "7.0.2",
71
+ "vitest": "5.0.5"
72
+ },
73
+ "scripts": {
74
+ "build": "tsdown",
75
+ "test": "vitest run",
76
+ "test:watch": "vitest",
77
+ "test:coverage": "vitest run --coverage",
78
+ "lint": "biome check src tests",
79
+ "lint:fix": "biome check --write src tests",
80
+ "format": "biome format --write src tests",
81
+ "typecheck": "tsc --noEmit"
82
+ }
83
+ }