{"version":3,"file":"_walk-bounds.mjs","sources":["../../../../src/core/actions/_walk-bounds.ts"],"sourcesContent":["import { SdkError } from '../sdk-error'\n\n/**\n * The bounds every keyset walk runs under: a page ceiling and a cancellation\n * signal.\n *\n * All six walkers — `callList` / `fetchList` on both API versions, and the v3\n * `callTail` / `fetchTail` — page with `while (true)`. Four exits existed: a\n * short page, an empty page, a soft error, and an unreadable cursor. #493 added\n * a fifth for a cursor that stops moving.\n *\n * None of them bounds a walk whose cursor *does* move. A method with more rows\n * than anyone expected, or a filter that matched far more than intended, keeps\n * issuing requests against the customer's portal, which is the thing the\n * restriction manager exists to prevent. (#495 added a sixth exit for a cursor\n * that moves *backwards*, which is how a cycle is caught now — but only for\n * cursor values the SDK can order, so the ceiling is still what remains for the\n * rest.) And once started, a walk could not be stopped: there was\n * nowhere to hand a signal (#484).\n *\n * ## Why the ceiling errors rather than truncating\n *\n * Returning what was collected would hand back a list that is short and looks\n * complete — the failure mode this class of bug keeps producing, and the one\n * `JSSDK_ACTION_CURSOR_STALLED` already refuses to produce. A caller who wants\n * the pages that did arrive walks with `fetchList` / `fetchTail`, which yield\n * each page before this throws.\n *\n * ## Why the default is 10 000\n *\n * It is a backstop, not a policy: it should bound a runaway without capping a\n * read anyone actually performs.\n *\n * - On `restApi:v2` a page is 50 rows, so 10 000 pages is **500 000 rows** —\n *   past the point where `callList`, which holds every row in memory, is the\n *   right tool at all. On `restApi:v3` the page size is capped **per method**\n *   rather than at a global 1000, and the default is 50 there too, so the\n *   ceiling lands in the same place for a walk that does not raise `limit`.\n * - At the default `drainRate` of 2 requests/second\n *   (`ParamsFactory.getDefault()`), 10 000 requests is about **83 minutes**. So\n *   the worst case a runaway can inflict on a portal changes from unbounded to\n *   an hour and a half — and a caller who hits the ceiling learns which method\n *   did it.\n *\n * Raise it with `maxPages` when a read genuinely needs more; there is no cap on\n * what you may pass.\n */\n\n/**\n * Page ceiling applied when a caller names none.\n *\n * @see the rationale in this module's docblock — the number is chosen to be\n *   unreachable by a legitimate read, not to express a policy.\n */\nexport const DEFAULT_MAX_PAGES = 10_000\n\n/**\n * Called after each page an **eager** walker collects (`callList`, `callTail`).\n *\n * Counts, not a percentage. The deprecated `callListMethod` reported percent\n * complete because it paged by offset and the v2 envelope carried `total`;\n * cursor paging reads neither. `restApi:v3` sends no `total` at all, and on v2\n * the walk never asks for one — `getTotal()` needs a request this walk does not\n * make. A denominator would therefore have to be invented, and a made-up\n * percentage is worse than an honest count.\n *\n * The streaming walkers take no `progress`: `fetchList` / `fetchTail` hand the\n * consumer each page as it arrives, so counting them is the consumer's own\n * `for await` body.\n *\n * Called synchronously, and its return value is ignored — a throwing callback\n * would abort the walk, so keep it to reporting. To stop a walk, use `signal`.\n */\nexport type WalkProgress = (progress: {\n  /** Pages read so far, including this one. */\n  pages: number\n  /** Rows collected so far, including this page. */\n  rows: number\n}) => void\n\n/** Options every keyset walker accepts, on both API versions. */\nexport type WalkBoundsOptions = {\n  /**\n   * Stop after this many pages and throw `JSSDK_ACTION_MAX_PAGES_EXCEEDED`.\n   * Defaults to {@link DEFAULT_MAX_PAGES}. Must be a positive integer.\n   */\n  maxPages?: number\n  /**\n   * Abort the walk. Throws `JSSDK_ACTION_ABORTED`.\n   *\n   * Checked at the top of each iteration — so an already-aborted signal costs\n   * no request at all — and again in the streaming walkers right after the page\n   * is yielded.\n   *\n   * The second check saves no request: nothing between a response and the next\n   * request spends a round trip. It buys the right *diagnosis*. An earlier\n   * revision dropped it on the reasoning that the gap after `yield` is empty\n   * and no test could tell the two apart. The gap is not empty — the stall\n   * guard and the page ceiling both sit in it — so a consumer that aborted\n   * while holding a page was told to raise `maxPages`, or that the cursor had\n   * stalled, when in fact it had cancelled. Reproduced with `maxPages: 2` and\n   * an abort during the hold of page 2.\n   *\n   * Polled rather than subscribed: `addEventListener` would need tearing down\n   * on every exit path, and there are eight.\n   */\n  signal?: AbortSignal\n}\n\n/**\n * Validate `maxPages` and fall back to the default.\n *\n * Refuses a non-integer, zero or negative value rather than coercing it: `0`\n * would mean \"walk nothing\", which no caller means, and a fractional ceiling\n * would fire at a page number nobody wrote.\n */\nexport function resolveMaxPages(action: string, maxPages: number | undefined): number {\n  if (undefined === maxPages) {\n    return DEFAULT_MAX_PAGES\n  }\n\n  if (!Number.isInteger(maxPages) || maxPages < 1) {\n    throw new SdkError({\n      code: 'JSSDK_ACTION_INVALID_MAX_PAGES',\n      description: `${action}: \\`maxPages\\` must be a positive integer — it is the number of pages the walk may read before it gives up. Omit it to use the default of ${DEFAULT_MAX_PAGES}.`,\n      status: 400\n    })\n  }\n\n  return maxPages\n}\n\n/**\n * The error a walk raises when it reaches its page ceiling.\n *\n * The method name is interpolated and nothing else. `SdkError` descriptions are\n * **not** run through `redactSensitiveParams` (see `SECURITY.md`), so a cursor\n * value, a filter or a row read off the response must never reach this text —\n * a REST method name is none of those.\n */\nexport function maxPagesExceededError(action: string, method: string, maxPages: number): SdkError {\n  return new SdkError({\n    code: 'JSSDK_ACTION_MAX_PAGES_EXCEEDED',\n    description: `${action}: stopped after ${maxPages} pages of \\`${method}\\` without reaching the end of the data. `\n      + `Either the read is genuinely larger than the ceiling — raise \\`maxPages\\` — or the walk is not making progress, `\n      + `which happens when the page condition is not applied and the cursor repeats values whose order the SDK cannot judge `\n      + `(a cursor that demonstrably moves backwards is reported earlier, as JSSDK_ACTION_CURSOR_WENT_BACKWARDS). `\n      + `The eager helpers (callList / callTail) return the pages they did read, with this error `\n      + `attached — so check \\`isSuccess\\` rather than assuming a returned list is whole. `\n      + `A streaming helper (fetchList / fetchTail) has already yielded every page it read, and `\n      + `throws this instead.`,\n    status: 500\n  })\n}\n\n/**\n * The error a walk raises when its `signal` fires.\n *\n * `status: 400`: the caller asked for this, so it is not a portal fault and not\n * an SDK fault. It is still an error rather than a quiet stop, for the same\n * reason the ceiling is — a partial list must not be mistaken for a whole one.\n * That is what the flag is for; the rows themselves are real and are handed\n * back by the eager helpers rather than discarded.\n */\nexport function walkAbortedError(action: string, method: string): SdkError {\n  return new SdkError({\n    code: 'JSSDK_ACTION_ABORTED',\n    description: `${action}: the walk over \\`${method}\\` was aborted through its \\`signal\\`. `\n      + `The eager helpers (callList / callTail) return the pages they did read, with this error `\n      + `attached; a streaming helper (fetchList / fetchTail) has already yielded every page it read, `\n      + `and throws this instead.`,\n    status: 400\n  })\n}\n\n/** Throw {@link walkAbortedError} if `signal` has already fired. */\nexport function assertNotAborted(\n  signal: AbortSignal | undefined,\n  action: string,\n  method: string\n): void {\n  if (true === signal?.aborted) {\n    throw walkAbortedError(action, method)\n  }\n}\n\n/**\n * The two codes a walk raises when it stops on a bound the caller set, rather\n * than on anything wrong with the data.\n *\n * They are handled apart from every other failure because the rows collected up\n * to that point are **correct** — merely incomplete. A stalled cursor is not in\n * this set: there the extra rows are duplicates of ones already held, so there\n * is nothing worth handing back.\n */\nconst WALK_BOUNDS_ERROR_CODES: ReadonlySet<string> = new Set([\n  'JSSDK_ACTION_MAX_PAGES_EXCEEDED',\n  'JSSDK_ACTION_ABORTED'\n])\n\n/**\n * Whether `error` is a walk stopping on one of its own bounds.\n *\n * The eager walkers use this to attach the error to their `Result` and return\n * what they read, the way they already do for a soft error from the portal. The\n * streaming walkers let it through: their consumer has each page as it arrives,\n * so there is nothing left to hand back.\n */\nexport function isWalkBoundsError(error: unknown): error is SdkError {\n  return error instanceof SdkError && WALK_BOUNDS_ERROR_CODES.has(error.code)\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAsDO,MAAM,iBAAA,GAAoB;AA8D1B,SAAS,eAAA,CAAgB,QAAgB,QAAA,EAAsC;AACpF,EAAA,IAAI,WAAc,QAAA,EAAU;AAC1B,IAAA,OAAO,iBAAA;AAAA,EACT;AAEA,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,QAAQ,CAAA,IAAK,WAAW,CAAA,EAAG;AAC/C,IAAA,MAAM,IAAI,QAAA,CAAS;AAAA,MACjB,IAAA,EAAM,gCAAA;AAAA,MACN,WAAA,EAAa,CAAA,EAAG,MAAM,CAAA,+IAAA,EAA6I,iBAAiB,CAAA,CAAA,CAAA;AAAA,MACpL,MAAA,EAAQ;AAAA,KACT,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,QAAA;AACT;AAdgB,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;AAwBT,SAAS,qBAAA,CAAsB,MAAA,EAAgB,MAAA,EAAgB,QAAA,EAA4B;AAChG,EAAA,OAAO,IAAI,QAAA,CAAS;AAAA,IAClB,IAAA,EAAM,iCAAA;AAAA,IACN,aAAa,CAAA,EAAG,MAAM,CAAA,gBAAA,EAAmB,QAAQ,eAAe,MAAM,CAAA,ypBAAA,CAAA;AAAA,IAQtE,MAAA,EAAQ;AAAA,GACT,CAAA;AACH;AAbgB,MAAA,CAAA,qBAAA,EAAA,uBAAA,CAAA;AAwBT,SAAS,gBAAA,CAAiB,QAAgB,MAAA,EAA0B;AACzE,EAAA,OAAO,IAAI,QAAA,CAAS;AAAA,IAClB,IAAA,EAAM,sBAAA;AAAA,IACN,WAAA,EAAa,CAAA,EAAG,MAAM,CAAA,kBAAA,EAAqB,MAAM,CAAA,oPAAA,CAAA;AAAA,IAIjD,MAAA,EAAQ;AAAA,GACT,CAAA;AACH;AATgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA;AAYT,SAAS,gBAAA,CACd,MAAA,EACA,MAAA,EACA,MAAA,EACM;AACN,EAAA,IAAI,IAAA,KAAS,QAAQ,OAAA,EAAS;AAC5B,IAAA,MAAM,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AAAA,EACvC;AACF;AARgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA;AAmBhB,MAAM,uBAAA,uBAAmD,GAAA,CAAI;AAAA,EAC3D,iCAAA;AAAA,EACA;AACF,CAAC,CAAA;AAUM,SAAS,kBAAkB,KAAA,EAAmC;AACnE,EAAA,OAAO,KAAA,YAAiB,QAAA,IAAY,uBAAA,CAAwB,GAAA,CAAI,MAAM,IAAI,CAAA;AAC5E;AAFgB,MAAA,CAAA,iBAAA,EAAA,mBAAA,CAAA;;;;"}