{"version":3,"file":"_cursor-progress.mjs","sources":["../../../../src/core/actions/_cursor-progress.ts"],"sourcesContent":["/** Which way a walk paginates: `ASC` ids grow, `DESC` ids shrink. */\nexport type CursorDirection = 'ASC' | 'DESC'\n\n/**\n * An ISO-8601 datetime, capturing the two things that have to match before two\n * of them can be ordered as text: the date/time separator, and the zone.\n *\n * The **zone is mandatory**, which is the point. A rendering that states no\n * offset at all — `2024-10-27 02:15:00`, what a MySQL `DATETIME` column prints —\n * repeats the wall-clock hour when the clocks go back, so 02:15 standard time\n * is a later instant than 02:30 summer time and yet sorts before it. Making the\n * zone optional would have let that pair through under \"same zone: neither has\n * one\", which is the very shape this check exists to decline.\n *\n * The separator is captured for the same reason: `'T'` is `0x54` and `' '` is\n * `0x20`, so a `T`-form value sorts after *every* space-form value with the same\n * date, whatever the time says.\n */\nconst ISO_DATETIME = /^\\d{4}-\\d{2}-\\d{2}([T ])\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$/\n\nconst DIGITS_ONLY = /^\\d+$/\n\n/**\n * Which way does a walk ordered by `order` paginate?\n *\n * Shared so that the string sent to the server and the direction the cursor is\n * audited against can never disagree. If they did, the SDK would ask the portal\n * to page one way and then reject every correct answer for going the other —\n * failing the walks that work, which is the one outcome this guard must not\n * produce.\n */\nexport function resolveCursorDirection(order: string): CursorDirection {\n  return DESC_ORDER.test(order) ? 'DESC' : 'ASC'\n}\n\n/** Matches the `DESC` half of an `order` value, however the caller spelled it. */\nexport const DESC_ORDER = /desc/i\n\n/**\n * Can these two cursor values be ordered against each other with confidence?\n *\n * Numbers: both finite. Strings: only the two shapes where JS code-unit order\n * provably matches the order the server sorted by.\n *\n * - **Digits only, same length** — a zero-padded id. Same length is what makes\n *   lexicographic and numeric order agree; `'9'` vs `'10'` is false one way and\n *   true the other, so unpadded ids are declined.\n * - **ISO-8601 datetimes stated in the same zone.** The zone matters: across a\n *   DST transition `2024-10-27T02:59:00+02:00` (00:59Z) is *earlier* than\n *   `2024-10-27T02:00:00+01:00` (01:00Z), yet sorts after it as text. Both are\n *   the same length, so length alone would not have caught it.\n *\n * Everything else is declined — deliberately, and letters are the reason. A\n * mixed-case `cursorField` under MySQL's default case-insensitive collation\n * orders `a1` before `B1`; JS orders `'B1'` before `'a1'`. There is no way to\n * tell from the values which collation sorted them, so a walk over such a field\n * must not be called backwards.\n */\nfunction isComparableCursorPair(next: number | string, previous: number | string): boolean {\n  if ('number' === typeof next && 'number' === typeof previous) {\n    return Number.isFinite(next) && Number.isFinite(previous)\n  }\n\n  if ('string' === typeof next && 'string' === typeof previous) {\n    if (next.length !== previous.length) {\n      return false\n    }\n\n    if (DIGITS_ONLY.test(next) && DIGITS_ONLY.test(previous)) {\n      return true\n    }\n\n    const nextIso = ISO_DATETIME.exec(next)\n    const previousIso = ISO_DATETIME.exec(previous)\n\n    return null !== nextIso\n      && null !== previousIso\n      && nextIso[1] === previousIso[1]\n      && nextIso[2] === previousIso[2]\n  }\n\n  return false\n}\n\n/**\n * Did the cursor move in the direction the walk paginates in?\n *\n * Every keyset walk here asks the server for rows **past** a value: the emulated\n * list walkers append `[cursorIdKey, '>', cursor]`, the native tail walkers send\n * `cursor: { field, value, order }`, which the server reads as `field > value`\n * for `ASC` and `field < value` for `DESC`. So in an answer that honoured the\n * condition the next cursor is strictly past the last one — always, on every\n * page. A value that is equal, or on the wrong side, means the condition was not\n * applied.\n *\n * That is what makes a direction check worth more than the equality check it\n * replaces, and worth more than the alternatives #495 weighed:\n *\n * - it catches a **cycle of any length**. A server alternating `A, B, A, B` never\n *   repeats the immediately preceding value, so the equality check never fires —\n *   but a cycle has to step backwards somewhere, and the first backwards step is\n *   this one. A ring of the last *N* cursors catches cycles up to *N* and costs\n *   memory; this catches all of them and costs nothing;\n * - it costs **no allocation**. The objection that deferred #495 — needing a set\n *   of every cursor seen, an unbounded allocation added to guard against an\n *   unbounded allocation — does not apply to a comparison of two values;\n * - it catches a cursor that simply **moves backwards**, which loses rows\n *   silently rather than looping, and which nothing looked for before.\n *\n * **Only when the two values are comparable**, and that is a deliberately narrow\n * set — see {@link isComparableCursorPair} for what qualifies and why. A cursor\n * is read off the response, and a `cursorField` naming something the SDK cannot\n * order — or a server changing `100` to `'100'` — must not be reported as a\n * failure to advance. Everything the check declines falls back to the equality\n * test this generalises, which is exactly the behaviour it had before.\n *\n * Strings in particular are judged only where JS order is certain to agree with\n * the server's. Every other pair is answered by returning `true` rather than by\n * guessing: a guard that stops a healthy walk is worse than one that misses a\n * sick one, and the equality check underneath still catches a true repeat.\n *\n * Returns `true` when the walk may continue, `false` when it cannot make\n * progress. Equality always answers `false`, whatever the types — that is the\n * check this generalises, and it holds for pairs the direction test declines\n * to judge.\n *\n * @param next - The cursor read from the page just received.\n * @param previous - The cursor that page was requested with.\n * @param direction - Which way this walk paginates.\n */\nexport function cursorProgressed(\n  next: number | string,\n  previous: number | string,\n  direction: CursorDirection\n): boolean {\n  if (next === previous) {\n    return false\n  }\n\n  if (!isComparableCursorPair(next, previous)) {\n    // A pair whose order the SDK cannot vouch for: not equal, and not\n    // something to call backwards. The walk continues, as it did before this\n    // check existed.\n    return true\n  }\n\n  return 'DESC' === direction ? next < previous : next > previous\n}\n"],"names":[],"mappings":";;;;;;;;;;AAkBA,MAAM,YAAA,GAAe,0EAAA;AAErB,MAAM,WAAA,GAAc,OAAA;AAWb,SAAS,uBAAuB,KAAA,EAAgC;AACrE,EAAA,OAAO,UAAA,CAAW,IAAA,CAAK,KAAK,CAAA,GAAI,MAAA,GAAS,KAAA;AAC3C;AAFgB,MAAA,CAAA,sBAAA,EAAA,wBAAA,CAAA;AAKT,MAAM,UAAA,GAAa;AAsB1B,SAAS,sBAAA,CAAuB,MAAuB,QAAA,EAAoC;AACzF,EAAA,IAAI,QAAA,KAAa,OAAO,IAAA,IAAQ,QAAA,KAAa,OAAO,QAAA,EAAU;AAC5D,IAAA,OAAO,OAAO,QAAA,CAAS,IAAI,CAAA,IAAK,MAAA,CAAO,SAAS,QAAQ,CAAA;AAAA,EAC1D;AAEA,EAAA,IAAI,QAAA,KAAa,OAAO,IAAA,IAAQ,QAAA,KAAa,OAAO,QAAA,EAAU;AAC5D,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,QAAA,CAAS,MAAA,EAAQ;AACnC,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,YAAY,IAAA,CAAK,IAAI,KAAK,WAAA,CAAY,IAAA,CAAK,QAAQ,CAAA,EAAG;AACxD,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,OAAA,GAAU,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA;AACtC,IAAA,MAAM,WAAA,GAAc,YAAA,CAAa,IAAA,CAAK,QAAQ,CAAA;AAE9C,IAAA,OAAO,IAAA,KAAS,OAAA,IACX,IAAA,KAAS,WAAA,IACT,QAAQ,CAAC,CAAA,KAAM,WAAA,CAAY,CAAC,CAAA,IAC5B,OAAA,CAAQ,CAAC,CAAA,KAAM,YAAY,CAAC,CAAA;AAAA,EACnC;AAEA,EAAA,OAAO,KAAA;AACT;AAxBS,MAAA,CAAA,sBAAA,EAAA,wBAAA,CAAA;AAwEF,SAAS,gBAAA,CACd,IAAA,EACA,QAAA,EACA,SAAA,EACS;AACT,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI,CAAC,sBAAA,CAAuB,IAAA,EAAM,QAAQ,CAAA,EAAG;AAI3C,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA,KAAW,SAAA,GAAY,IAAA,GAAO,QAAA,GAAW,IAAA,GAAO,QAAA;AACzD;AAjBgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA;;;;"}