effect-tsでRequirements Managementやりたいが、うまくいかない箇所がある

ソースコードをうまくまとめようとしてRequirementsの機能を使ってみたが、いまいちうまくいかない箇所がある。動作させることはできるが、型のチェックが弱い気がする

# 前回

https://qiita.com/kuangzhixitian3/items/4499c58bf7d4108791b1

## 共通部

カスタムエラークラスやログ処理の共通クラス

```ts
class SleepNGBeforeError {
  static tagName = 'Sleep.Ng.Before'
  readonly _tag = SleepNGBeforeError.tagName

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

  getMessage(site: string) {
    return `${site} - ng - Sleep NG before wait`
  }
}

class SleepNGAfterError {
  static tagName = 'Sleep.Ng.After'
  readonly _tag = SleepNGAfterError.tagName

  wait: number
  constructor(w: number) {
    this.wait = w
  }

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

  getMessage(site: string) {
    return `${site} - ng - ${this.wait} - Sleep NG after wait`
  }
}

class AlreadyAbortError {
  static tagName = 'Already.Abort.Error'
  readonly _tag = AlreadyAbortError.tagName

  static isClass(test: any): test is AlreadyAbortError {
    return test._tag === AlreadyAbortError.tagName
  }

  static getMessage(site: string) {
    return `${site} - aborted - from event`
  }
}

class OriginAbortError {
  static tagName = 'Origin.Abort.Error'
  readonly _tag = OriginAbortError.tagName

  site: string
  reason: string
  constructor(s: string, r: string) {
    this.site = s
    this.reason = r
  }

  static isClass(test: any): test is OriginAbortError {
    return test._tag === OriginAbortError.tagName
  }

  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)) {
        originAbortError = new OriginAbortError(site, `${lastIndex} is NaN`)
      } else if (lastIndex === undefined) {
        originAbortError = new OriginAbortError(site, `${lastIndex} is undefined`)
      } else if (site[site.length - 2] !== '/') {
        originAbortError = new OriginAbortError(site, `lastIndex >= 10`)
      } else if (topIndex && topIndex + lastIndex >= 10) {
        originAbortError = new OriginAbortError(site, `topIndex + lastIndex >= 10`)
      }

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

      return Effect.succeed(site)
    }
  }
}

class OkData {
  wait: number
  constructor(w: number) {
    this.wait = w
  }

  static isClass(test: any): test is OkData {
    return test instanceof OkData
  }

  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>(
  site: string,
  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 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAll = mapError_sleepNg(site, sleepEffect)
    return effectAll
  })
```


## 変更したかった箇所

```ts
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(site))
    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('使い方１ー１:mapper1_gen - 全部同時実行', 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-02-28T14:09:49.893Z - text: [start]
time : 2025-02-28T14:09:49.907Z - text: https://test/10 - abort - lastIndex >= 10
time : 2025-02-28T14:09:49.908Z - text: https://test/1 - aborted - from event
time : 2025-02-28T14:09:49.908Z - text: https://test/2 - aborted - from event
time : 2025-02-28T14:09:49.908Z - text: https://test/3 - aborted - from event
time : 2025-02-28T14:09:49.908Z - text: https://test/4 - aborted - from event
time : 2025-02-28T14:09:49.909Z - text: https://test/5 - aborted - from event
中断しました: All fibers interrupted without errors.
```

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

```ts
time : 2025-02-28T14:10:40.470Z - text: [start]
time : 2025-02-28T14:10:40.483Z - text: https://test/3 - ng - Sleep NG before wait
time : 2025-02-28T14:10:41.482Z - text: https://test/1 - ok - 1000
time : 2025-02-28T14:10:42.486Z - text: https://test/2 - ng - 2000 - Sleep NG after wait
time : 2025-02-28T14:10:44.483Z - text: https://test/4 - ng - 4000 - Sleep NG after wait
time : 2025-02-28T14:10:45.495Z - text: https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 2000 }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGBeforeError { _tag: 'Sleep.Ng.Before' }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 4000 }
  },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```



## 変更案１

型を変更できるようにしたかったが、Implementation関連で型エラーがでてas anyで逃げる方法しか思いつかなかったもの

```ts
class Abort_Service extends Context.Tag('Abort_Service')<
  Abort_Service,
  {
    readonly return_abort_error: <E1>() => Either.Either<never, E1>
    readonly abort_event: () => void
  }
>() {}

class Main_Service extends Context.Tag('Main_Service')<
  Main_Service,
  {
    readonly abort_check: <A1, E2>(
      abortAction: Function,
      topIndex?: number
    ) => Effect.Effect<A1, E2, never>
    readonly do_effect: <A1, A2, E3>(
      abort_result: A1,
      site: string
    ) => Effect.Effect<Effect.Effect<A2, UnknownException | E3, never>, UnknownException, never>
  }
>() {}

class Result_Service extends Context.Tag('Result_Service')<
  Result_Service,
  {
    readonly ok_action: <A2>(okData: A2, site: string) => void
    readonly ng_action: <E3>(ngData: UnknownException | E3, site: string) => void
  }
>() {}

const mapper1_service =
  <A1, A2, E1, E2, E3>(abortController: AbortController, topIndex?: number) =>
  (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<E1>()
      }
      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<A1, E2>(abortAction, topIndex)
      const effect = yield* mainService.do_effect<A1, A2, E3>(abortCheck, site)
      const either = yield* Effect.either(effect)

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

      removeEvent()
      return either
    })

const mapper1_runnable = (abortController: AbortController, topIndex?: number) => (site: string) =>
  mapper1_service<
    string,
    OkData,
    AlreadyAbortError,
    OriginAbortError,
    SleepNGBeforeError | SleepNGAfterError
  >(
    abortController,
    topIndex
  )(site).pipe(
    Effect.provideService(Abort_Service, {
      return_abort_error: () => {
        const alreadyAbortError = new AlreadyAbortError()
        Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
        return Either.left(alreadyAbortError) as any
      },
      abort_event: () => Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
    }),
    Effect.provideService(Main_Service, {
      abort_check: (abortAction: Function, topIndex?: number) =>
        pipe(site, OriginAbortError.siteCheck(abortAction, topIndex)) as any,
      do_effect: (abortResult: any, site: string) =>
        pipe(abortResult, OkData.getLastIndex, getWaitNumber, do_sleep1(site)) as any
    }),
    Effect.provideService(Result_Service, {
      ok_action: (okData: any, site: string) => {
        const message = okData.getMessage(site)
        Effect.runPromise(checkLog(message))
      },
      ng_action: (ngData: UnknownException | any, site: string) => {
        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))
        }
      }
    })
  )

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー２:mapper1_gen - 全部同時実行', 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_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-02-28T14:16:52.418Z - text: [start]
time : 2025-02-28T14:16:52.433Z - text: https://test/10 - abort - lastIndex >= 10
time : 2025-02-28T14:16:52.434Z - text: https://test/1 - aborted - from event
time : 2025-02-28T14:16:52.435Z - text: https://test/2 - aborted - from event
time : 2025-02-28T14:16:52.435Z - text: https://test/3 - aborted - from event
time : 2025-02-28T14:16:52.435Z - text: https://test/4 - aborted - from event
time : 2025-02-28T14:16:52.435Z - text: https://test/5 - aborted - from event
中断しました: All fibers interrupted without errors.
```

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

```ts
time : 2025-02-28T14:17:39.930Z - text: [start]
time : 2025-02-28T14:17:39.944Z - text: https://test/3 - ng - Sleep NG before wait
time : 2025-02-28T14:17:40.948Z - text: https://test/1 - ok - 1000
time : 2025-02-28T14:17:41.948Z - text: https://test/2 - ng - 2000 - Sleep NG after wait
time : 2025-02-28T14:17:43.950Z - text: https://test/4 - ng - 4000 - Sleep NG after wait
time : 2025-02-28T14:17:44.946Z - text: https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 2000 }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGBeforeError { _tag: 'Sleep.Ng.Before' }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 4000 }
  },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```



## 変更案２

型を変更できないようにしたが、Implementation関連で想定と異なる型をいれた場合にエラーが出ないことがあった
※AlreadyAbortError　と　OriginAbortError　を間違えてもエラーにならなかったりする


```ts
type A1 = string
type A2 = OkData
type E1 = AlreadyAbortError
type E2 = OriginAbortError
type E3 = SleepNGBeforeError | SleepNGAfterError

class Abort_Service extends Context.Tag('Abort_Service')<
  Abort_Service,
  {
    readonly return_abort_error: () => Either.Either<never, E1>
    readonly abort_event: () => void
  }
>() {}

class Main_Service extends Context.Tag('Main_Service')<
  Main_Service,
  {
    readonly abort_check: (abortAction: Function, topIndex?: number) => Effect.Effect<A1, E2, never>
    readonly do_effect: (
      abort_result: A1,
      site: string
    ) => Effect.Effect<Effect.Effect<A2, UnknownException | E3, never>, UnknownException, never>
  }
>() {}

class Result_Service extends Context.Tag('Result_Service')<
  Result_Service,
  {
    readonly ok_action: (okData: A2, site: string) => void
    readonly ng_action: (ngData: UnknownException | E3, site: string) => void
  }
>() {}

const mapper1_service = (abortController: AbortController, topIndex?: number) => (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, site)
    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
  })

const mapper1_runnable = (abortController: AbortController, topIndex?: number) => (site: string) =>
  mapper1_service(
    abortController,
    topIndex
  )(site).pipe(
    Effect.provideService(Abort_Service, {
      return_abort_error: () => {
        const alreadyAbortError = new AlreadyAbortError()
        Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
        return Either.left(alreadyAbortError)
      },
      abort_event: () => Effect.runPromise(checkLog(AlreadyAbortError.getMessage(site)))
    }),
    Effect.provideService(Main_Service, {
      abort_check: (abortAction: Function, topIndex?: number) =>
        pipe(site, OriginAbortError.siteCheck(abortAction, topIndex)),
      do_effect: (abortResult: A1, site: string) =>
        pipe(abortResult, OkData.getLastIndex, getWaitNumber, do_sleep1(site))
    }),
    Effect.provideService(Result_Service, {
      ok_action: (okData: A2, site: string) => {
        const message = okData.getMessage(site)
        Effect.runPromise(checkLog(message))
      },
      ng_action: (ngData: UnknownException | E3, site: string) => {
        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))
        }
      }
    })
  )

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー３:mapper1_gen - 全部同時実行', 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_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-02-28T14:19:56.975Z - text: [start]
time : 2025-02-28T14:19:56.990Z - text: https://test/10 - abort - lastIndex >= 10
time : 2025-02-28T14:19:56.991Z - text: https://test/1 - aborted - from event
time : 2025-02-28T14:19:56.992Z - text: https://test/2 - aborted - from event
time : 2025-02-28T14:19:56.992Z - text: https://test/3 - aborted - from event
time : 2025-02-28T14:19:56.992Z - text: https://test/4 - aborted - from event
time : 2025-02-28T14:19:56.992Z - text: https://test/5 - aborted - from event
中断しました: All fibers interrupted without errors.
```

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

```ts
time : 2025-02-28T14:20:07.775Z - text: [start]
time : 2025-02-28T14:20:07.791Z - text: https://test/3 - ng - Sleep NG before wait
time : 2025-02-28T14:20:08.788Z - text: https://test/1 - ok - 1000
time : 2025-02-28T14:20:09.805Z - text: https://test/2 - ng - 2000 - Sleep NG after wait
time : 2025-02-28T14:20:11.806Z - text: https://test/4 - ng - 4000 - Sleep NG after wait
time : 2025-02-28T14:20:12.805Z - text: https://test/5 - ok - 5000
[
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 1000 } },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 2000 }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGBeforeError { _tag: 'Sleep.Ng.Before' }
  },
  {
    _id: 'Either',
    _tag: 'Left',
    left: SleepNGAfterError { _tag: 'Sleep.Ng.After', wait: 4000 }
  },
  { _id: 'Either', _tag: 'Right', right: OkData { wait: 5000 } }
]
```