effect-tsでRequirements ManagementでDI

ソースコードをうまくまとめようとしてRequirementsの機能を使ってみて、いくつか改善できた

# 前回

https://qiita.com/kuangzhixitian3/items/867207c38d35861d1434

# 参考

https://www.typeonce.dev/course/effect-beginners-complete-getting-started/effect-services/define-services-with-context-tag

## Requirements Managementを使わない場合

テストするときに非同期処理で待ち時間があるため、テストが多くなった場合に、処理時間が長くなってしまう。それを変更場合も以下の形式だと少々面倒

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Either, Exit, Cause, Data, Schema } from 'effect'
import { UnknownException } from 'effect/Cause'

class SleepNGBeforeError extends Data.TaggedError('SleepNGBeforeError')<{}> {
  getMessage(site: string) {
    return `${site} - ng - Sleep NG before wait`
  }

  static isClass(test: any): test is SleepNGBeforeError {
    return test._tag === SleepNGBeforeError.name
  }
}

class SleepNGAfterError extends Data.TaggedError('SleepNGAfterError')<{ wait: number }> {
  getMessage(site: string) {
    return `${site} - ng - ${this.wait} - Sleep NG after wait`
  }

  static isClass(test: any): test is SleepNGAfterError {
    return test._tag === SleepNGAfterError.name
  }
}

class AlreadyAbortError extends Data.TaggedError('AlreadyAbortError')<{}> {
  static getMessage(site: string) {
    return `${site} - aborted - from event`
  }
}

class OriginAbortError extends Data.TaggedError('OriginAbortError')<{
  site: string
  reason: string
}> {
  getMessage() {
    return `${this.site} - abort - ${this.reason}`
  }

  static siteCheck(abortAction: Function, topIndex?: number) {
    return (site: string) => {
      const lastIndex = OkData.getLastIndex(site)
      let originAbortError = undefined
      if (Number.isNaN(lastIndex)) {
        const reason = `${lastIndex} is NaN`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (lastIndex === undefined) {
        const reason = `${lastIndex} is undefined`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (site[site.length - 2] !== '/') {
        const reason = `lastIndex >= 10`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (topIndex && topIndex + lastIndex >= 10) {
        const reason = `topIndex + lastIndex >= 10`
        originAbortError = new OriginAbortError({ site, reason })
      }

      if (originAbortError !== undefined) {
        Effect.runPromise(checkLog(originAbortError.getMessage()))
        abortAction()
        return Effect.fail(originAbortError)
      }

      return Effect.succeed(site)
    }
  }
}

class OkData extends Schema.Class<OkData>('OkData')({
  wait: Schema.Number
}) {
  static getLastIndex(site: string) {
    const index = site.slice(-1)
    return Number(index)
  }

  getMessage(site: string) {
    return `${site} - ok - ${this.wait}`
  }
}

const sleep = (wait: number) =>
  Effect.tryPromise<OkData>(
    () =>
      new Promise((resolve, reject) => {
        const waitSeconds = wait / 1000
        if (waitSeconds % 3 === 0) {
          reject(new SleepNGBeforeError())
        } else if (waitSeconds % 2 === 0) {
          setTimeout(() => reject(new SleepNGAfterError({ wait })), wait)
        } else {
          setTimeout(() => resolve(new OkData({ wait })), wait)
        }
      })
  )

const checkLog = (text: string) =>
  Effect.sync(() => console.log(`time : ${new Date().toISOString()} - text: ${text}`))

const testOptions = {
  timeout: 1000 * 100
}

function getWaitNumber(index: number) {
  const wait = index * 1000
  return wait
}

function mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = Effect.mapError(effectAndThen, (err) => {
    if (SleepNGBeforeError.isClass(err.error)) {
      return err.error
    } else if (SleepNGAfterError.isClass(err.error)) {
      return err.error
    } else {
      Effect.runPromise(checkLog(`mapError_sleepNg : UnknownException : ${err}`))
      return err
    }
  })
  return effectAll
}

const do_sleep1 = (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAll = mapError_sleepNg(sleepEffect)
    return effectAll
  })

const mapper1_gen = (abortController: AbortController, topIndex?: number) => (site: string) =>
  Effect.gen(function* () {
    if (abortController.signal.aborted) {
      const alreadyAbortError = new AlreadyAbortError()
      Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
      return Either.left(alreadyAbortError)
    }

    const abortEvent = () => {
      Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
    }
    abortController.signal.addEventListener('abort', abortEvent)
    function removeEvent() {
      abortController.signal.removeEventListener('abort', abortEvent)
    }
    const abortAction = () => {
      removeEvent()
      abortController.abort()
    }

    const abortCheck = yield* pipe(site, OriginAbortError.siteCheck(abortAction, topIndex))
    const effect = yield* pipe(abortCheck, OkData.getLastIndex, getWaitNumber, do_sleep1)
    const either = yield* Effect.either(effect)

    if (Either.isRight(either)) {
      const okData = either.right

      const message = okData.getMessage(site)
      Effect.runPromise(checkLog(message))
    } else if (Either.isLeft(either)) {
      const ngData = either.left

      if (SleepNGBeforeError.isClass(ngData)) {
        const message = ngData.getMessage(site)
        Effect.runPromise(checkLog(message))
      } else if (SleepNGAfterError.isClass(ngData)) {
        const message = ngData.getMessage(site)
        Effect.runPromise(checkLog(message))
      }
    }

    removeEvent()
    return either
  })

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー１: Requirements管理しない場合', testOptions, async () => {
    const sites = [
      'https://test/1',
      'https://test/2',
      'https://test/3',
      'https://test/4',
      'https://test/5',
      'https://test/10'
    ]

    const abortController = new AbortController()

    Effect.runPromise(checkLog('[start]'))

    const effect = await Effect.all(sites.map(mapper1_gen(abortController)), {
      concurrency: sites.length
    })
    const exit = await Effect.runPromiseExit(effect, { signal: abortController.signal })

    console.log(
      Exit.match(exit, {
        onFailure: (cause) => `中断しました: ${Cause.pretty(cause)}`,
        onSuccess: (either) => either
      })
    )
  })
})
```

実行結果

```ts
time : 2025-03-02T14:35:31.379Z - text: [start]
time : 2025-03-02T14:35:31.390Z - text: https://test/10 - abort - lastIndex >= 10
time : 2025-03-02T14:35:31.391Z - text: https://test/1 - aborted - from event
time : 2025-03-02T14:35:31.391Z - text: https://test/2 - aborted - from event
time : 2025-03-02T14:35:31.392Z - text: https://test/3 - aborted - from event
time : 2025-03-02T14:35:31.392Z - text: https://test/4 - aborted - from event
time : 2025-03-02T14:35:31.392Z - text: https://test/5 - aborted - from event
中断しました: All fibers interrupted without errors.
```

実行結果（※中断データを除外した場合）

```ts
time : 2025-03-02T14:35:45.788Z - text: [start]
time : 2025-03-02T14:35:45.801Z - text: https://test/3 - ng - Sleep NG before wait
time : 2025-03-02T14:35:46.829Z - text: https://test/1 - ok - 1000
time : 2025-03-02T14:35:47.822Z - text: https://test/2 - ng - 2000 - Sleep NG after wait
time : 2025-03-02T14:35:49.828Z - text: https://test/4 - ng - 4000 - Sleep NG after wait
time : 2025-03-02T14:35:50.816Z - text: https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: { wait: 2000, _tag: 'SleepNGAfterError' }
  },
  { _id: 'Either', _tag: 'Left', left: { _tag: 'SleepNGBeforeError' } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: { wait: 4000, _tag: 'SleepNGAfterError' }
  },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```


## Requirements Managementを使う場合１：（非同期のまま）

詳細テストをやる場合はなどは以下のような非同期のままテストするとよさそう
Requirements Managementを使う場合２との違いはserviceの切り替えだけなので、interfaceが変わらない限り、それぞれへの変更影響がない

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Either, Exit, Cause, Context, Data, Schema } from 'effect'
import { UnknownException } from 'effect/Cause'

class SleepNGBeforeError extends Data.TaggedError('SleepNGBeforeError')<{}> {
  getMessage(site: string) {
    return `${site} - ng - Sleep NG before wait`
  }

  static isClass(test: any): test is SleepNGBeforeError {
    return test._tag === SleepNGBeforeError.name
  }
}

class SleepNGAfterError extends Data.TaggedError('SleepNGAfterError')<{ wait: number }> {
  getMessage(site: string) {
    return `${site} - ng - ${this.wait} - Sleep NG after wait`
  }

  static isClass(test: any): test is SleepNGAfterError {
    return test._tag === SleepNGAfterError.name
  }
}

class AlreadyAbortError extends Data.TaggedError('AlreadyAbortError')<{}> {
  static getMessage(site: string) {
    return `${site} - aborted - from event`
  }
}

class OriginAbortError extends Data.TaggedError('OriginAbortError')<{
  site: string
  reason: string
}> {
  getMessage() {
    return `${this.site} - abort - ${this.reason}`
  }

  static siteCheck(abortAction: Function, topIndex?: number) {
    return (site: string) => {
      const lastIndex = OkData.getLastIndex(site)
      let originAbortError = undefined
      if (Number.isNaN(lastIndex)) {
        const reason = `${lastIndex} is NaN`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (lastIndex === undefined) {
        const reason = `${lastIndex} is undefined`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (site[site.length - 2] !== '/') {
        const reason = `lastIndex >= 10`
        originAbortError = new OriginAbortError({ site, reason })
      } else if (topIndex && topIndex + lastIndex >= 10) {
        const reason = `topIndex + lastIndex >= 10`
        originAbortError = new OriginAbortError({ site, reason })
      }

      if (originAbortError !== undefined) {
        Effect.runPromise(checkLog(originAbortError.getMessage()))
        abortAction()
        return Effect.fail(originAbortError)
      }

      return Effect.succeed(site)
    }
  }
}

class OkData extends Schema.Class<OkData>('OkData')({
  wait: Schema.Number
}) {
  static getLastIndex(site: string) {
    const index = site.slice(-1)
    return Number(index)
  }

  getMessage(site: string) {
    return `${site} - ok - ${this.wait}`
  }
}

const sleep = (wait: number) =>
  Effect.tryPromise<OkData>(
    () =>
      new Promise((resolve, reject) => {
        const waitSeconds = wait / 1000
        if (waitSeconds % 3 === 0) {
          reject(new SleepNGBeforeError())
        } else if (waitSeconds % 2 === 0) {
          setTimeout(() => reject(new SleepNGAfterError({ wait })), wait)
        } else {
          setTimeout(() => resolve(new OkData({ wait })), wait)
        }
      })
  )

const checkLog = (text: string) =>
  Effect.sync(() => console.log(`time : ${new Date().toISOString()} - text: ${text}`))

const testOptions = {
  timeout: 1000 * 100
}

function getWaitNumber(index: number) {
  const wait = index * 1000
  return wait
}

function mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = Effect.mapError(effectAndThen, (err) => {
    if (SleepNGBeforeError.isClass(err.error)) {
      return err.error
    } else if (SleepNGAfterError.isClass(err.error)) {
      return err.error
    } else {
      Effect.runPromise(checkLog(`mapError_sleepNg : UnknownException : ${err}`))
      return err
    }
  })
  return effectAll
}

const do_sleep1 = (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAll = mapError_sleepNg(sleepEffect)
    return effectAll
  })

interface Abort_Service_Interface<E1> {
  readonly return_abort_error: () => Either.Either<never, E1>
  readonly abort_event: () => void
}

interface Main_Service_Interface<A1, A2, E2, E3> {
  readonly abort_check: (abortAction: Function, topIndex?: number) => Effect.Effect<A1, E2, never>
  readonly do_effect: (
    abort_result: A1
  ) => Effect.Effect<Effect.Effect<A2, UnknownException | E3, never>, UnknownException, never>
}

interface Result_Service_Interface<A2, E3> {
  readonly ok_action: (okData: A2, site: string) => void
  readonly ng_action: (ngData: UnknownException | E3, site: string) => void
}

namespace Product_Service {
  class Abort_Service extends Context.Tag('Abort_Service')<
    Abort_Service,
    Abort_Service_Interface<AlreadyAbortError>
  >() {
    static readonly Live = (site: string) =>
      Abort_Service.of({
        return_abort_error: function () {
          const alreadyAbortError = new AlreadyAbortError()
          Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
          return Either.left(alreadyAbortError)
        },
        abort_event: function () {
          return Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
        }
      })
  }

  class Main_Service extends Context.Tag('Main_Service')<
    Main_Service,
    Main_Service_Interface<string, OkData, OriginAbortError, SleepNGBeforeError | SleepNGAfterError>
  >() {
    static readonly Live = (site: string) =>
      Main_Service.of({
        abort_check: (abortAction, topIndex?) =>
          pipe(site, OriginAbortError.siteCheck(abortAction, topIndex)),
        do_effect: (abortResult) => pipe(abortResult, OkData.getLastIndex, getWaitNumber, do_sleep1)
      })
  }

  class Result_Service extends Context.Tag('Result_Service')<
    Result_Service,
    Result_Service_Interface<OkData, SleepNGBeforeError | SleepNGAfterError>
  >() {
    static readonly Live = Result_Service.of({
      ok_action: (okData, site) => {
        const message = okData.getMessage(site)
        Effect.runPromise(checkLog(message))
      },
      ng_action: (ngData, site) => {
        if (SleepNGBeforeError.isClass(ngData)) {
          const message = ngData.getMessage(site)
          Effect.runPromise(checkLog(message))
        } else if (SleepNGAfterError.isClass(ngData)) {
          const message = ngData.getMessage(site)
          Effect.runPromise(checkLog(message))
        }
      }
    })
  }

  export class Mapper1 {
    private static mapper1_service(abortController: AbortController, topIndex?: number) {
      return (site: string) =>
        Effect.gen(function* () {
          const abortService = yield* Abort_Service
          const mainService = yield* Main_Service
          const resultService = yield* Result_Service

          if (abortController.signal.aborted) {
            return abortService.return_abort_error()
          }
          abortController.signal.addEventListener('abort', abortService.abort_event)
          function removeEvent() {
            abortController.signal.removeEventListener('abort', abortService.abort_event)
          }
          const abortAction = () => {
            removeEvent()
            abortController.abort()
          }

          const abortCheck = yield* mainService.abort_check(abortAction, topIndex)
          const effect = yield* mainService.do_effect(abortCheck)
          const either = yield* Effect.either(effect)

          if (Either.isRight(either)) {
            const okData = either.right
            resultService.ok_action(okData, site)
          } else if (Either.isLeft(either)) {
            const ngData = either.left
            resultService.ng_action(ngData, site)
          }

          removeEvent()
          return either
        })
    }

    static mapper1_runnable(abortController: AbortController, topIndex?: number) {
      return (site: string) =>
        Mapper1.mapper1_service(
          abortController,
          topIndex
        )(site).pipe(
          Effect.provideService(Abort_Service, Abort_Service.Live(site)),
          Effect.provideService(Main_Service, Main_Service.Live(site)),
          Effect.provideService(Result_Service, Result_Service.Live)
        )
    }
  }
}

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー２: Requirements管理:本番or非同期テスト用', testOptions, async () => {
    const sites = [
      'https://test/1',
      'https://test/2',
      'https://test/3',
      'https://test/4',
      'https://test/5',
      'https://test/10'
    ]

    const abortController = new AbortController()

    Effect.runPromise(checkLog('[start]'))

    const effect = await Effect.all(
      sites.map(Product_Service.Mapper1.mapper1_runnable(abortController)),
      {
        concurrency: sites.length
      }
    )
    const exit = await Effect.runPromiseExit(effect, { signal: abortController.signal })

    console.log(
      Exit.match(exit, {
        onFailure: (cause) => `中断しました: ${Cause.pretty(cause)}`,
        onSuccess: (either) => either
      })
    )
  })
})
```

実行結果

```ts
time : 2025-03-02T14:40:29.875Z - text: [start]
time : 2025-03-02T14:40:29.888Z - text: https://test/10 - abort - lastIndex >= 10
time : 2025-03-02T14:40:29.890Z - text: https://test/1 - aborted - from event
time : 2025-03-02T14:40:29.890Z - text: https://test/2 - aborted - from event
time : 2025-03-02T14:40:29.890Z - text: https://test/3 - aborted - from event
time : 2025-03-02T14:40:29.890Z - text: https://test/4 - aborted - from event
time : 2025-03-02T14:40:29.890Z - text: https://test/5 - aborted - from event
中断しました: All fibers interrupted without errors.
```

実行結果（※中断データを除外した場合）

```ts
time : 2025-03-02T14:40:43.621Z - text: [start]
time : 2025-03-02T14:40:43.637Z - text: https://test/3 - ng - Sleep NG before wait
time : 2025-03-02T14:40:44.639Z - text: https://test/1 - ok - 1000
time : 2025-03-02T14:40:45.649Z - text: https://test/2 - ng - 2000 - Sleep NG after wait
time : 2025-03-02T14:40:47.639Z - text: https://test/4 - ng - 4000 - Sleep NG after wait
time : 2025-03-02T14:40:48.644Z - text: https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: { wait: 2000, _tag: 'SleepNGAfterError' }
  },
  { _id: 'Either', _tag: 'Left', left: { _tag: 'SleepNGBeforeError' } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: { wait: 4000, _tag: 'SleepNGAfterError' }
  },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```


## Requirements Managementを使う場合２：（同期に変更し、簡素化）

自動テストなどはこれを使えば、テストがいっぱいあってもテスト時間は短くて済む
また、処理も非同期など複雑なところを省略できるので、変更時にテスト確認が容易になる

```ts
import { describe, test } from 'vitest'
import { Effect, Either, Exit, Cause, Context, Schema } from 'effect'
import { UnknownException } from 'effect/Cause'

class OkData extends Schema.Class<OkData>('OkData')({
  wait: Schema.Number
}) {
  static getLastIndex(site: string) {
    const index = site.slice(-1)
    return Number(index)
  }

  getMessage(site: string) {
    return `${site} - ok - ${this.wait}`
  }
}

const checkLog = (text: string) =>
  Effect.sync(() => console.log(`time : ${new Date().toISOString()} - text: ${text}`))

const testOptions = {
  timeout: 1000 * 100
}

interface Abort_Service_Interface<E1> {
  readonly return_abort_error: () => Either.Either<never, E1>
  readonly abort_event: () => void
}

interface Main_Service_Interface<A1, A2, E2, E3> {
  readonly abort_check: (abortAction: Function, topIndex?: number) => Effect.Effect<A1, E2, never>
  readonly do_effect: (
    abort_result: A1
  ) => Effect.Effect<Effect.Effect<A2, UnknownException | E3, never>, UnknownException, never>
}

interface Result_Service_Interface<A2, E3> {
  readonly ok_action: (okData: A2, site: string) => void
  readonly ng_action: (ngData: UnknownException | E3, site: string) => void
}

namespace Test_Service {
  class Test_Abort_Service extends Context.Tag('Test_Abort_Service')<
    Test_Abort_Service,
    Abort_Service_Interface<'Test.AlreadyAbortError'>
  >() {
    static readonly Live = (site: string) =>
      Test_Abort_Service.of({
        return_abort_error: function () {
          console.log(`${site} - aborted - from event - test - return_abort_error`)
          return Either.left('Test.AlreadyAbortError')
        },
        abort_event: function () {
          return console.log(`${site} - aborted - from event - test - abort_event`)
        }
      })
  }

  class Test_Main_Service extends Context.Tag('Test_Main_Service')<
    Test_Main_Service,
    Main_Service_Interface<
      string,
      OkData,
      'Test.OriginAbortError',
      'Test.SleepNGBeforeError' | 'Test.SleepNGAfterError'
    >
  >() {
    static readonly Live = (site: string) =>
      Test_Main_Service.of({
        abort_check: (abortAction, topIndex?) => {
          const index = site.slice(-1)
          const lastIndex = Number(index)
          let originAbortError = undefined
          let reason = undefined
          if (Number.isNaN(lastIndex)) {
            reason = `${lastIndex} is NaN`
            originAbortError = 'Test.OriginAbortError'
          } else if (lastIndex === undefined) {
            reason = `${lastIndex} is undefined`
            originAbortError = 'Test.OriginAbortError'
          } else if (site[site.length - 2] !== '/') {
            reason = `lastIndex >= 10`
            originAbortError = 'Test.OriginAbortError'
          } else if (topIndex && topIndex + lastIndex >= 10) {
            reason = `topIndex + lastIndex >= 10`
            originAbortError = 'Test.OriginAbortError'
          }

          if (originAbortError !== undefined) {
            console.log(`${site} - abort - ${reason}`)
            abortAction()
            return Effect.fail('Test.OriginAbortError')
          }

          return Effect.succeed(site)
        },
        do_effect: (abortResult) => {
          const index = abortResult.slice(-1)
          const lastIndex = Number(index)

          if (lastIndex % 3 === 0) {
            return Effect.succeed(Effect.fail('Test.SleepNGBeforeError'))
          } else if (lastIndex % 2 === 0) {
            return Effect.succeed(Effect.fail('Test.SleepNGAfterError'))
          } else {
            return Effect.succeed(Effect.succeed(new OkData({ wait: lastIndex * 1000 })))
          }
        }
      })
  }

  class Test_Result_Service extends Context.Tag('Test_Result_Service')<
    Test_Result_Service,
    Result_Service_Interface<OkData, 'Test.SleepNGBeforeError' | 'Test.SleepNGAfterError'>
  >() {
    static readonly Live = Test_Result_Service.of({
      ok_action: (okData, site) => {
        const message = okData.getMessage(site)
        console.log(message)
      },
      ng_action: (ngData, site) => {
        const message = `${site} - ng - ${ngData}`
        console.log(message)
      }
    })
  }

  export class Mapper1 {
    private static mapper1_service(abortController: AbortController, topIndex?: number) {
      return (site: string) =>
        Effect.gen(function* () {
          const abortService = yield* Test_Abort_Service
          const mainService = yield* Test_Main_Service
          const resultService = yield* Test_Result_Service

          if (abortController.signal.aborted) {
            return abortService.return_abort_error()
          }
          abortController.signal.addEventListener('abort', abortService.abort_event)
          function removeEvent() {
            abortController.signal.removeEventListener('abort', abortService.abort_event)
          }
          const abortAction = () => {
            removeEvent()
            abortController.abort()
          }

          const abortCheck = yield* mainService.abort_check(abortAction, topIndex)
          const effect = yield* mainService.do_effect(abortCheck)
          const either = yield* Effect.either(effect)

          if (Either.isRight(either)) {
            const okData = either.right
            resultService.ok_action(okData, site)
          } else if (Either.isLeft(either)) {
            const ngData = either.left
            resultService.ng_action(ngData, site)
          }

          removeEvent()
          return either
        })
    }

    static mapper1_runnable_test(abortController: AbortController, topIndex?: number) {
      return (site: string) =>
        Mapper1.mapper1_service(
          abortController,
          topIndex
        )(site).pipe(
          Effect.provideService(Test_Abort_Service, Test_Abort_Service.Live(site)),
          Effect.provideService(Test_Main_Service, Test_Main_Service.Live(site)),
          Effect.provideService(Test_Result_Service, Test_Result_Service.Live)
        )
    }
  }
}

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー３: Requirements管理:高速テスト用', testOptions, async () => {
    const sites = [
      'https://test/1',
      'https://test/2',
      'https://test/3',
      'https://test/4',
      'https://test/5',
      'https://test/10'
    ]

    const abortController = new AbortController()

    Effect.runPromise(checkLog('[start]'))

    const effect = await Effect.all(
      sites.map(Test_Service.Mapper1.mapper1_runnable_test(abortController)),
      {
        concurrency: sites.length
      }
    )
    const exit = await Effect.runPromiseExit(effect, { signal: abortController.signal })

    console.log(
      Exit.match(exit, {
        onFailure: (cause) => `中断しました: ${Cause.pretty(cause)}`,
        onSuccess: (either) => either
      })
    )
  })
})
```

実行結果

```ts
time : 2025-03-02T14:45:17.943Z - text: [start]
https://test/1 - ok - 1000
https://test/2 - ng - Test.SleepNGAfterError
https://test/3 - ng - Test.SleepNGBeforeError
https://test/4 - ng - Test.SleepNGAfterError
https://test/5 - ok - 5000
https://test/10 - abort - lastIndex >= 10
中断しました: All fibers interrupted without errors.
```

実行結果（※中断データを除外した場合）

```ts
time : 2025-03-02T14:45:31.946Z - text: [start]
https://test/1 - ok - 1000
https://test/2 - ng - Test.SleepNGAfterError
https://test/3 - ng - Test.SleepNGBeforeError
https://test/4 - ng - Test.SleepNGAfterError
https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  { _id: 'Either', _tag: 'Left', left: 'Test.SleepNGAfterError' },
  { _id: 'Either', _tag: 'Left', left: 'Test.SleepNGBeforeError' },
  { _id: 'Either', _tag: 'Left', left: 'Test.SleepNGAfterError' },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```