id: no-three-sequential-awaits
language: Tsx
severity: error
message: 'Avoid 3 sequential await statements; abstract dependent work or parallelize independent work with Promise.all().'
files:
  - '**/*.ts'
  - '**/*.mts'
  - '**/*.cts'
  - '**/*.tsx'
ignores:
  - '**/*.test.ts'
  - '**/*.test.mts'
  - '**/*.test.cts'
  - '**/*.spec.ts'
  - '**/*.spec.mts'
  - '**/*.spec.cts'
  - '**/*.mock.test.ts'
  - '**/*.mock.test.mts'
  - '**/*.mock.test.cts'
  - '**/*.stories.ts'
  - '**/*.stories.mts'
  - backend/test-helpers/**
  - test-helpers/**
  - test-infra/**
  - web/test-helpers/**
  - '**/test-helpers/**'
  - playwright/**
  - playwright/setup/**
  - integration-tests/**
  - seed/**
  - '**/seeds/**'
  - backend/scripts/seeds/**
  - backend/scripts/seed/**
  - backend/scripts/explain-analyze/**
  - '**/*.test.tsx'
  - '**/*.spec.tsx'
  - '**/*.mock.test.tsx'
  - '**/*.stories.tsx'
rule:
  all:
    - not:
        any:
          - matches: async-disposable-acquisition
          - matches: async-disposable-commit
    - not:
        follows:
          stopBy: neighbor
          any:
            - all:
                - pattern:
                    context: await $P
                    selector: expression_statement
                - not:
                    pattern: await params
                - not:
                    pattern: await searchParams
                - not:
                    has:
                      pattern: Promise.all($$)
                - not:
                    has:
                      pattern: Promise.allSettled($$)
            - all:
                - pattern: const $W = await $P
                - not:
                    pattern: const $W = await params
                - not:
                    pattern: const $W = await searchParams
                - not:
                    pattern: const $W = await Promise.all($$$)
                - not:
                    pattern: const $W = await Promise.allSettled($$$)
            - all:
                - pattern: let $W = await $P
                - not:
                    pattern: let $W = await params
                - not:
                    pattern: let $W = await searchParams
                - not:
                    pattern: let $W = await Promise.all($$$)
                - not:
                    pattern: let $W = await Promise.allSettled($$$)
            - all:
                - pattern: var $W = await $P
                - not:
                    pattern: var $W = await params
                - not:
                    pattern: var $W = await searchParams
                - not:
                    pattern: var $W = await Promise.all($$$)
                - not:
                    pattern: var $W = await Promise.allSettled($$$)
            - all:
                - pattern:
                    context: $W = await $P
                    selector: expression_statement
                - not:
                    pattern: $W = await params
                - not:
                    pattern: $W = await searchParams
                - not:
                    pattern: $W = await Promise.all($$$)
                - not:
                    pattern: $W = await Promise.allSettled($$$)
          not:
            any:
              - matches: async-disposable-acquisition
              - matches: async-disposable-commit
    - any:
        - all:
            - pattern:
                context: await $A
                selector: expression_statement
            - not:
                pattern: await params
            - not:
                pattern: await searchParams
            - not:
                has:
                  pattern: Promise.all($$)
            - not:
                has:
                  pattern: Promise.allSettled($$)
        - all:
            - pattern: const $X = await $A
            - not:
                pattern: const $X = await params
            - not:
                pattern: const $X = await searchParams
            - not:
                pattern: const $X = await Promise.all($$$)
            - not:
                pattern: const $X = await Promise.allSettled($$$)
        - all:
            - pattern: let $X = await $A
            - not:
                pattern: let $X = await params
            - not:
                pattern: let $X = await searchParams
            - not:
                pattern: let $X = await Promise.all($$$)
            - not:
                pattern: let $X = await Promise.allSettled($$$)
        - all:
            - pattern: var $X = await $A
            - not:
                pattern: var $X = await params
            - not:
                pattern: var $X = await searchParams
            - not:
                pattern: var $X = await Promise.all($$$)
            - not:
                pattern: var $X = await Promise.allSettled($$$)
        - all:
            - pattern:
                context: $X = await $A
                selector: expression_statement
            - not:
                pattern: $X = await params
            - not:
                pattern: $X = await searchParams
            - not:
                pattern: $X = await Promise.all($$$)
            - not:
                pattern: $X = await Promise.allSettled($$$)
    - precedes:
        stopBy: neighbor
        all:
          - any:
              - all:
                  - pattern:
                      context: await $B
                      selector: expression_statement
                  - not:
                      pattern: await params
                  - not:
                      pattern: await searchParams
                  - not:
                      has:
                        pattern: Promise.all($$)
                  - not:
                      has:
                        pattern: Promise.allSettled($$)
              - all:
                  - pattern: const $Y = await $B
                  - not:
                      pattern: const $Y = await params
                  - not:
                      pattern: const $Y = await searchParams
                  - not:
                      pattern: const $Y = await Promise.all($$$)
                  - not:
                      pattern: const $Y = await Promise.allSettled($$$)
              - all:
                  - pattern: let $Y = await $B
                  - not:
                      pattern: let $Y = await params
                  - not:
                      pattern: let $Y = await searchParams
                  - not:
                      pattern: let $Y = await Promise.all($$$)
                  - not:
                      pattern: let $Y = await Promise.allSettled($$$)
              - all:
                  - pattern: var $Y = await $B
                  - not:
                      pattern: var $Y = await params
                  - not:
                      pattern: var $Y = await searchParams
                  - not:
                      pattern: var $Y = await Promise.all($$$)
                  - not:
                      pattern: var $Y = await Promise.allSettled($$$)
              - all:
                  - pattern:
                      context: $Y = await $B
                      selector: expression_statement
                  - not:
                      pattern: $Y = await params
                  - not:
                      pattern: $Y = await searchParams
                  - not:
                      pattern: $Y = await Promise.all($$$)
                  - not:
                      pattern: $Y = await Promise.allSettled($$$)
          - not:
              any:
                - matches: async-disposable-acquisition
                - matches: async-disposable-commit
          - precedes:
              stopBy: neighbor
              not:
                any:
                  - matches: async-disposable-acquisition
                  - matches: async-disposable-commit
              any:
                - all:
                    - pattern:
                        context: await $C
                        selector: expression_statement
                    - not:
                        pattern: await params
                    - not:
                        pattern: await searchParams
                    - not:
                        has:
                          pattern: Promise.all($$)
                    - not:
                        has:
                          pattern: Promise.allSettled($$)
                - all:
                    - pattern: const $Z = await $C
                    - not:
                        pattern: const $Z = await params
                    - not:
                        pattern: const $Z = await searchParams
                    - not:
                        pattern: const $Z = await Promise.all($$$)
                    - not:
                        pattern: const $Z = await Promise.allSettled($$$)
                - all:
                    - pattern: let $Z = await $C
                    - not:
                        pattern: let $Z = await params
                    - not:
                        pattern: let $Z = await searchParams
                    - not:
                        pattern: let $Z = await Promise.all($$$)
                    - not:
                        pattern: let $Z = await Promise.allSettled($$$)
                - all:
                    - pattern: var $Z = await $C
                    - not:
                        pattern: var $Z = await params
                    - not:
                        pattern: var $Z = await searchParams
                    - not:
                        pattern: var $Z = await Promise.all($$$)
                    - not:
                        pattern: var $Z = await Promise.allSettled($$$)
                - all:
                    - pattern:
                        context: $Z = await $C
                        selector: expression_statement
                    - not:
                        pattern: $Z = await params
                    - not:
                        pattern: $Z = await searchParams
                    - not:
                        pattern: $Z = await Promise.all($$$)
                    - not:
                        pattern: $Z = await Promise.allSettled($$$)
constraints:
  A:
    not:
      regex: '^Promise\.all(Settled)?\b'
  B:
    not:
      regex: '^Promise\.all(Settled)?\b'
  C:
    not:
      regex: '^Promise\.all(Settled)?\b'
  P:
    not:
      regex: '^Promise\.all(Settled)?\b'
utils:
  async-disposable-acquisition:
    all:
      - pattern:
          context: |
            async function run() {
              await using $RESOURCE = await $ACQUIRE
            }
          selector: expression_statement
      - precedes:
          # Pair only with the nearest lifecycle endpoint to the right.
          # Overlapping-resource policy belongs to a separate lifecycle rule.
          stopBy:
            any:
              - pattern:
                  context: |
                    async function run() {
                      await using $_ = await $_
                    }
                  selector: expression_statement
              - pattern:
                  context: |
                    async function run() {
                      await $_.commit($$$)
                    }
                  selector: expression_statement
          pattern:
            context: |
              async function run() {
                await $RESOURCE.commit($$$)
              }
            selector: expression_statement
  async-disposable-commit:
    all:
      - pattern:
          context: |
            async function run() {
              await $RESOURCE.commit($$$)
            }
          selector: expression_statement
      - follows:
          # Pair only with the nearest lifecycle endpoint to the left.
          # Overlapping-resource policy belongs to a separate lifecycle rule.
          stopBy:
            any:
              - pattern:
                  context: |
                    async function run() {
                      await using $_ = await $_
                    }
                  selector: expression_statement
              - pattern:
                  context: |
                    async function run() {
                      await $_.commit($$$)
                    }
                  selector: expression_statement
          pattern:
            context: |
              async function run() {
                await using $RESOURCE = await $ACQUIRE
              }
            selector: expression_statement
examples:
  - code: |
      async function run() {
        await one()
        await two()
        await three()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        const one = await loadOne()
        const two = await loadTwo()
        target.value = await loadThree()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await one()
        await two()
      }
    isValid: true
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await one()
        if (condition) await two()
        await three()
      }
    isValid: true
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await Promise.all([one(), two(), three()])
      }
    isValid: true
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
      }
    isValid: true
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginBoundedTransaction({ timeoutMs: 1000 })
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await transaction.commit()
      }
    isValid: true
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await applyThirdChange(transaction)
        await transaction.commit()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await unrelatedFirst()
        await unrelatedSecond()
        await unrelatedThird()
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await other.commit()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
        await afterFirst()
        await afterSecond()
        await afterThird()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using outer = await acquire()
        {
          await using inner = await acquire()
          await innerFirst(inner)
          await innerSecond(inner)
          await innerThird(inner)
          await inner.commit()
        }
        await outer.commit()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await transaction.rollback()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await using resource = await acquire()
        await useFirst(resource)
        await useSecond(resource)
        await resource.dispose()
      }
    isValid: false
    file: 'backend/services/example.mts'
  - code: |
      async function run() {
        await one()
        await two()
        await three()
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        const { id } = await params
        const topic = await getTopic(id)
        const aliases = await getAliases(id)
        return <div>{topic.name}{aliases.length}</div>
      }
    isValid: true
    file: web/app/example.tsx
  - code: |
      async function run() {
        await Promise.all([one(), two(), three()])
        return <div />
      }
    isValid: true
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
        return <div />
      }
    isValid: true
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginBoundedTransaction({ timeoutMs: 1000 })
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await transaction.commit()
        return <div />
      }
    isValid: true
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await applyThirdChange(transaction)
        await transaction.commit()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await unrelatedFirst()
        await unrelatedSecond()
        await unrelatedThird()
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await other.commit()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyChange(transaction)
        await transaction.commit()
        await afterFirst()
        await afterSecond()
        await afterThird()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using outer = await acquire()
        {
          await using inner = await acquire()
          await innerFirst(inner)
          await innerSecond(inner)
          await innerThird(inner)
          await inner.commit()
        }
        await outer.commit()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using transaction = await beginTransaction()
        await applyFirstChange(transaction)
        await applySecondChange(transaction)
        await transaction.rollback()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
  - code: |
      async function run() {
        await using resource = await acquire()
        await useFirst(resource)
        await useSecond(resource)
        await resource.dispose()
        return <div />
      }
    isValid: false
    file: web/app/example.tsx
