effect-tsで配列の配列の非同期処理

配列の配列の非同期処理のやり方のメモ

# effect-ts

https://effect.website/

# 前回

https://qiita.com/kuangzhixitian3/items/cd7cbfa27b2370987962

https://qiita.com/kuangzhixitian3/items/d98f577d33735eac27f0

## 配列が１つの場合

### 全部同時実行

```plantuml
@startuml
robust "全体" as all
concise "https://test/1" as test1
concise "https://test/2" as test2
concise "https://test/3" as test3
concise "https://test/4" as test4
concise "https://test/5" as test5

@all
0 is 待機中
+1 is 処理中
+5 is 待機中

@test1
1 is 処理中
+1 is {hidden}

@test2
1 is 処理中
+2 is {hidden}

@test3
1 is 処理中
+3 is {hidden}

@test4
1 is 処理中
+4 is {hidden}

@test5
1 is 処理中
+5 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方１ー全部同時実行', testOptions, async () => {
    const sites = [
      'https://test/1',
      'https://test/2',
      'https://test/3',
      'https://test/4',
      'https://test/5'
    ]

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

    const effect = await Effect.all(sites.map(mapper1_gen), {
      concurrency: sites.length
    })
    const either = await Effect.runPromise(effect)

    console.log(either)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:00:01.227Z - text: [start]
time : 2025-02-09T14:00:02.244Z - text: https://test/1
time : 2025-02-09T14:00:03.242Z - text: https://test/2
time : 2025-02-09T14:00:04.238Z - text: https://test/3
time : 2025-02-09T14:00:05.240Z - text: https://test/4
time : 2025-02-09T14:00:06.242Z - text: https://test/5
[
  { _id: 'Either', _tag: 'Right', right: 'https://test/1 - 1000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/2 - 2000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/3 - 3000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/4 - 4000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/5 - 5000' }
]
```


### 最大２個まで同時実行

```plantuml
@startuml
robust "全体" as all
concise "https://test/1" as test1
concise "https://test/2" as test2
concise "https://test/3" as test3
concise "https://test/4" as test4
concise "https://test/5" as test5

@all
0 is 待機中
+1 is 処理中
+9 is 待機中

@test1
1 is 処理中
+1 is {hidden}

@test2
1 is 処理中
+2 is {hidden}

@test3
2 is 処理中
+3 is {hidden}

@test4
3 is 処理中
+4 is {hidden}

@test5
5 is 処理中
+5 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

describe('effect-ts 非同期処理１つの配列', () => {
  test('使い方２ー最大２個まで同時実行', testOptions, async () => {
    const sites = [
      'https://test/1',
      'https://test/2',
      'https://test/3',
      'https://test/4',
      'https://test/5'
    ]

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

    const effect = await Effect.all(sites.map(mapper1_gen), {
      concurrency: 2
    })
    const either = await Effect.runPromise(effect)

    console.log(either)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:00:55.537Z - text: [start]
time : 2025-02-09T14:00:56.549Z - text: https://test/1
time : 2025-02-09T14:00:57.548Z - text: https://test/2
time : 2025-02-09T14:00:59.552Z - text: https://test/3
time : 2025-02-09T14:01:01.559Z - text: https://test/4
time : 2025-02-09T14:01:04.566Z - text: https://test/5
[
  { _id: 'Either', _tag: 'Right', right: 'https://test/1 - 1000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/2 - 2000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/3 - 3000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/4 - 4000' },
  { _id: 'Either', _tag: 'Right', right: 'https://test/5 - 5000' }
]
```


## 配列が２つの場合（シンプル）

### 全部同時実行からの全部同時実行

```plantuml
@startuml
robust "全体" as all
concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

@all
0 is 待機中
+1 is 処理中
+6 is 待機中

@test3
1 is 処理中
+3 is {hidden}

@test3_1
4 is 処理中
+1 is {hidden}

@test3_2
4 is 処理中
+2 is {hidden}

@test3_3
4 is 処理中
+3 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方１ー全部同時実行からの全部同時実行', testOptions, async () => {
    const sites = [
      // ['https://test/1/1'],
      // ['https://test/2/1', 'https://test/2/2']
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3']
      // ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      // [
      //   'https://test/5/1',
      //   'https://test/5/2',
      //   'https://test/5/3',
      //   'https://test/5/4',
      //   'https://test/5/5'
      // ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: Number.MAX_VALUE })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:03:03.421Z - text: [start]
time : 2025-02-09T14:03:06.445Z - text: mapper2-index = 3
time : 2025-02-09T14:03:07.464Z - text: https://test/3/1
time : 2025-02-09T14:03:08.456Z - text: https://test/3/2
time : 2025-02-09T14:03:09.454Z - text: https://test/3/3
[ 'https://test/3 - 6000' ]
```


### 全部同時実行からの直列実行

```plantuml
@startuml
robust "全体" as all
concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

@all
0 is 待機中
+1 is 処理中
+9 is 待機中

@test3
1 is 処理中
+3 is {hidden}

@test3_1
4 is 処理中
+1 is {hidden}

@test3_2
5 is 処理中
+2 is {hidden}

@test3_3
7 is 処理中
+3 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方３ー全部同時実行からの直列実行', testOptions, async () => {
    const sites = [
      // ['https://test/1/1'],
      // ['https://test/2/1', 'https://test/2/2']
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3']
      // ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      // [
      //   'https://test/5/1',
      //   'https://test/5/2',
      //   'https://test/5/3',
      //   'https://test/5/4',
      //   'https://test/5/5'
      // ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: 1 })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:04:24.205Z - text: [start]
time : 2025-02-09T14:04:27.218Z - text: mapper2-index = 3
time : 2025-02-09T14:04:28.225Z - text: https://test/3/1
time : 2025-02-09T14:04:30.227Z - text: https://test/3/2
time : 2025-02-09T14:04:33.231Z - text: https://test/3/3
[ 'https://test/3 - 9000' ]
```



### 全部同時実行からの最大２個まで同時実行

```plantuml
@startuml
robust "全体" as all
concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

@all
0 is 待機中
+1 is 処理中
+7 is 待機中

@test3
1 is 処理中
+3 is {hidden}

@test3_1
4 is 処理中
+1 is {hidden}

@test3_2
4 is 処理中
+2 is {hidden}

@test3_3
5 is 処理中
+3 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方５ー全部同時実行からの最大２個まで同時実行', testOptions, async () => {
    const sites = [
      // ['https://test/1/1'],
      // ['https://test/2/1', 'https://test/2/2']
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3']
      // ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      // [
      //   'https://test/5/1',
      //   'https://test/5/2',
      //   'https://test/5/3',
      //   'https://test/5/4',
      //   'https://test/5/5'
      // ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: 2 })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:05:46.110Z - text: [start]
time : 2025-02-09T14:05:49.124Z - text: mapper2-index = 3
time : 2025-02-09T14:05:50.134Z - text: https://test/3/1
time : 2025-02-09T14:05:51.146Z - text: https://test/3/2
time : 2025-02-09T14:05:53.143Z - text: https://test/3/3
[ 'https://test/3 - 7000' ]
```


## 配列が２つの場合（複雑）

### 全部同時実行からの全部同時実行

```plantuml
@startuml
robust "全体" as all

concise "https://test/1" as test1
concise "https://test/1/1" as test1_1

concise "https://test/2" as test2
concise "https://test/2/1" as test2_1
concise "https://test/2/2" as test2_2

concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

concise "https://test/4" as test4
concise "https://test/4/1" as test4_1
concise "https://test/4/2" as test4_2
concise "https://test/4/3" as test4_3
concise "https://test/4/4" as test4_4

concise "https://test/5" as test5
concise "https://test/5/1" as test5_1
concise "https://test/5/2" as test5_2
concise "https://test/5/3" as test5_3
concise "https://test/5/4" as test5_4
concise "https://test/5/5" as test5_5

@all
0 is 待機中
+1 is 処理中
+10 is 待機中

@test1
1 is 処理中
+1 is {hidden}
@test1_1
2 is 処理中
+1 is {hidden}

@test2
1 is 処理中
+2 is {hidden}
@test2_1
3 is 処理中
+1 is {hidden}
@test2_2
3 is 処理中
+2 is {hidden}

@test3
1 is 処理中
+3 is {hidden}
@test3_1
4 is 処理中
+1 is {hidden}
@test3_2
4 is 処理中
+2 is {hidden}
@test3_3
4 is 処理中
+3 is {hidden}

@test4
1 is 処理中
+4 is {hidden}
@test4_1
5 is 処理中
+1 is {hidden}
@test4_2
5 is 処理中
+2 is {hidden}
@test4_3
5 is 処理中
+3 is {hidden}
@test4_4
5 is 処理中
+4 is {hidden}

@test5
1 is 処理中
+5 is {hidden}
@test5_1
6 is 処理中
+1 is {hidden}
@test5_2
6 is 処理中
+2 is {hidden}
@test5_3
6 is 処理中
+3 is {hidden}
@test5_4
6 is 処理中
+4 is {hidden}
@test5_5
6 is 処理中
+5 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方２ー全部同時実行からの全部同時実行', testOptions, async () => {
    const sites = [
      ['https://test/1/1'],
      ['https://test/2/1', 'https://test/2/2'],
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3'],
      ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      [
        'https://test/5/1',
        'https://test/5/2',
        'https://test/5/3',
        'https://test/5/4',
        'https://test/5/5'
      ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: Number.MAX_VALUE })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:07:20.404Z - text: [start]
time : 2025-02-09T14:07:21.431Z - text: mapper2-index = 1
time : 2025-02-09T14:07:22.422Z - text: mapper2-index = 2
time : 2025-02-09T14:07:22.444Z - text: https://test/1/1
time : 2025-02-09T14:07:23.426Z - text: mapper2-index = 3
time : 2025-02-09T14:07:23.429Z - text: https://test/2/1
time : 2025-02-09T14:07:24.427Z - text: mapper2-index = 4
time : 2025-02-09T14:07:24.433Z - text: https://test/2/2
time : 2025-02-09T14:07:24.435Z - text: https://test/3/1
time : 2025-02-09T14:07:25.420Z - text: mapper2-index = 5
time : 2025-02-09T14:07:25.435Z - text: https://test/3/2
time : 2025-02-09T14:07:25.437Z - text: https://test/4/1
time : 2025-02-09T14:07:26.435Z - text: https://test/5/1
time : 2025-02-09T14:07:26.436Z - text: https://test/3/3
time : 2025-02-09T14:07:26.441Z - text: https://test/4/2
time : 2025-02-09T14:07:27.438Z - text: https://test/5/2
time : 2025-02-09T14:07:27.440Z - text: https://test/4/3
time : 2025-02-09T14:07:28.427Z - text: https://test/5/3
time : 2025-02-09T14:07:28.447Z - text: https://test/4/4
time : 2025-02-09T14:07:29.429Z - text: https://test/5/4
time : 2025-02-09T14:07:30.437Z - text: https://test/5/5
[
  'https://test/1 - 2000',
  'https://test/2 - 4000',
  'https://test/3 - 6000',
  'https://test/4 - 8000',
  'https://test/5 - 10000'
]
```


### 全部同時実行からの直列実行

```plantuml
@startuml
robust "全体" as all

concise "https://test/1" as test1
concise "https://test/1/1" as test1_1

concise "https://test/2" as test2
concise "https://test/2/1" as test2_1
concise "https://test/2/2" as test2_2

concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

concise "https://test/4" as test4
concise "https://test/4/1" as test4_1
concise "https://test/4/2" as test4_2
concise "https://test/4/3" as test4_3
concise "https://test/4/4" as test4_4

concise "https://test/5" as test5
concise "https://test/5/1" as test5_1
concise "https://test/5/2" as test5_2
concise "https://test/5/3" as test5_3
concise "https://test/5/4" as test5_4
concise "https://test/5/5" as test5_5

@all
0 is 待機中
+1 is 処理中
+20 is 待機中

@test1
1 is 処理中
+1 is {hidden}
@test1_1
2 is 処理中
+1 is {hidden}

@test2
1 is 処理中
+2 is {hidden}
@test2_1
3 is 処理中
+1 is {hidden}
@test2_2
4 is 処理中
+2 is {hidden}

@test3
1 is 処理中
+3 is {hidden}
@test3_1
4 is 処理中
+1 is {hidden}
@test3_2
5 is 処理中
+2 is {hidden}
@test3_3
7 is 処理中
+3 is {hidden}

@test4
1 is 処理中
+4 is {hidden}
@test4_1
5 is 処理中
+1 is {hidden}
@test4_2
6 is 処理中
+2 is {hidden}
@test4_3
8 is 処理中
+3 is {hidden}
@test4_4
11 is 処理中
+4 is {hidden}

@test5
1 is 処理中
+5 is {hidden}
@test5_1
6 is 処理中
+1 is {hidden}
@test5_2
7 is 処理中
+2 is {hidden}
@test5_3
9 is 処理中
+3 is {hidden}
@test5_4
12 is 処理中
+4 is {hidden}
@test5_5
16 is 処理中
+5 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方４ー全部同時実行からの直列実行', testOptions, async () => {
    const sites = [
      ['https://test/1/1'],
      ['https://test/2/1', 'https://test/2/2'],
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3'],
      ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      [
        'https://test/5/1',
        'https://test/5/2',
        'https://test/5/3',
        'https://test/5/4',
        'https://test/5/5'
      ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: 1 })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:09:36.479Z - text: [start]
time : 2025-02-09T14:09:37.495Z - text: mapper2-index = 1
time : 2025-02-09T14:09:38.494Z - text: mapper2-index = 2
time : 2025-02-09T14:09:38.510Z - text: https://test/1/1
time : 2025-02-09T14:09:39.493Z - text: mapper2-index = 3
time : 2025-02-09T14:09:39.509Z - text: https://test/2/1
time : 2025-02-09T14:09:40.496Z - text: mapper2-index = 4
time : 2025-02-09T14:09:40.499Z - text: https://test/3/1
time : 2025-02-09T14:09:41.505Z - text: mapper2-index = 5
time : 2025-02-09T14:09:41.507Z - text: https://test/4/1
time : 2025-02-09T14:09:41.520Z - text: https://test/2/2
time : 2025-02-09T14:09:42.512Z - text: https://test/3/2
time : 2025-02-09T14:09:42.514Z - text: https://test/5/1
time : 2025-02-09T14:09:43.523Z - text: https://test/4/2
time : 2025-02-09T14:09:44.529Z - text: https://test/5/2
time : 2025-02-09T14:09:45.519Z - text: https://test/3/3
time : 2025-02-09T14:09:46.529Z - text: https://test/4/3
time : 2025-02-09T14:09:47.534Z - text: https://test/5/3
time : 2025-02-09T14:09:50.537Z - text: https://test/4/4
time : 2025-02-09T14:09:51.546Z - text: https://test/5/4
time : 2025-02-09T14:09:56.548Z - text: https://test/5/5
[
  'https://test/1 - 2000',
  'https://test/2 - 5000',
  'https://test/3 - 9000',
  'https://test/4 - 14000',
  'https://test/5 - 20000'
]
```



### 全部同時実行からの最大２個まで同時実行

```plantuml
@startuml
robust "全体" as all

concise "https://test/1" as test1
concise "https://test/1/1" as test1_1

concise "https://test/2" as test2
concise "https://test/2/1" as test2_1
concise "https://test/2/2" as test2_2

concise "https://test/3" as test3
concise "https://test/3/1" as test3_1
concise "https://test/3/2" as test3_2
concise "https://test/3/3" as test3_3

concise "https://test/4" as test4
concise "https://test/4/1" as test4_1
concise "https://test/4/2" as test4_2
concise "https://test/4/3" as test4_3
concise "https://test/4/4" as test4_4

concise "https://test/5" as test5
concise "https://test/5/1" as test5_1
concise "https://test/5/2" as test5_2
concise "https://test/5/3" as test5_3
concise "https://test/5/4" as test5_4
concise "https://test/5/5" as test5_5

@all
0 is 待機中
+1 is 処理中
+14 is 待機中

@test1
1 is 処理中
+1 is {hidden}
@test1_1
2 is 処理中
+1 is {hidden}

@test2
1 is 処理中
+2 is {hidden}
@test2_1
3 is 処理中
+1 is {hidden}
@test2_2
3 is 処理中
+2 is {hidden}

@test3
1 is 処理中
+3 is {hidden}
@test3_1
4 is 処理中
+1 is {hidden}
@test3_2
4 is 処理中
+2 is {hidden}
@test3_3
5 is 処理中
+3 is {hidden}

@test4
1 is 処理中
+4 is {hidden}
@test4_1
5 is 処理中
+1 is {hidden}
@test4_2
5 is 処理中
+2 is {hidden}
@test4_3
6 is 処理中
+3 is {hidden}
@test4_4
7 is 処理中
+4 is {hidden}

@test5
1 is 処理中
+5 is {hidden}
@test5_1
6 is 処理中
+1 is {hidden}
@test5_2
6 is 処理中
+2 is {hidden}
@test5_3
7 is 処理中
+3 is {hidden}
@test5_4
8 is 処理中
+4 is {hidden}
@test5_5
10 is 処理中
+5 is {hidden}

@enduml
```

```ts
import { describe, test } from 'vitest'
import { Effect, pipe, Array, Either, Option } from 'effect'

const sleep = (wait: number) =>
  Effect.promise<string>(
    () =>
      new Promise((resolve) => {
        setTimeout(() => resolve(String(wait)), wait)
      })
  )

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

const testOptions = {
  timeout: 1000 * 100
}

const sumNumber = (chunkSize: number) => (a: number[], b: number[]) => {
  function addArray(num: number) {
    if (a.length === chunkSize) {
      const aMin = Math.min(...a)
      const aMinIndex = a.indexOf(aMin)
      a[aMinIndex] += num
    } else {
      a.push(num)
    }
  }

  b.forEach((bNum) => addArray(bNum))

  return a
}

function getLastIndex(site: string) {
  const index = site.slice(-1)
  return Number(index)
}
function getTopIndex(site: string) {
  const index = site.substring(site.length - 3, site.length - 2)
  return Number(index)
}

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

const do_sleep1 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      Effect.runPromise(checkLog(site))
      return `${site} - ${_wait_str}`
    })
    return effectAndThen
  })
function getWaitFromMapper1Message(mapper1Message: string) {
  const mapper1WaitString = mapper1Message.substring(mapper1Message.indexOf('-') + 1)
  const mapper1Wait = Number(mapper1WaitString)
  return mapper1Wait
}

const do_sleep2 = (site: string) => (wait: number) =>
  Effect.try(() => {
    const sleepEffect = sleep(wait)
    const effectAndThen = Effect.andThen(sleepEffect, (_wait_str) => {
      const index = getTopIndex(site)
      Effect.runPromise(checkLog(`mapper2-index = ${index}`))
      return (opWait: number) =>
        `${site.substring(0, site.length - 2)} - ${Number(_wait_str) + opWait}`
    })
    return effectAndThen
  })

const mapper1_gen = (site: string) =>
  Effect.gen(function* () {
    const effect = yield* pipe(site, getLastIndex, getWaitNumber, do_sleep1(site))
    const either = yield* Effect.either(effect)
    return either
  })

const mapper2_gen = (options: { concurrency: number }) => (sites: string[]) =>
  Effect.gen(function* () {
    const site = sites[0]
    const effect2 = yield* pipe(site, getTopIndex, getWaitNumber, do_sleep2(site))
    const either2 = yield* Effect.either(effect2)
    const right2 = Either.getRight(either2)

    const either1 = yield* Effect.all(sites.map(mapper1_gen), options)
    const arrayEither1 = Array.separate(either1)
    const rightArray1 = arrayEither1[1].map((mapper1Message) =>
      getWaitFromMapper1Message(mapper1Message)
    )
    const concurrency = options['concurrency']
    const chunkArray = Array.chunksOf(rightArray1, concurrency)
    const waitNumber = Array.reduce(chunkArray, [0], sumNumber(concurrency))

    const result = Option.match(right2, {
      onNone: () => 'None right1',
      onSome: (someRight2) => someRight2(Math.max(...waitNumber))
    })

    return result
  })

describe('effect-ts 非同期処理２つの配列', () => {
  test('使い方６ー全部同時実行からの最大２個まで同時実行', testOptions, async () => {
    const sites = [
      ['https://test/1/1'],
      ['https://test/2/1', 'https://test/2/2'],
      ['https://test/3/1', 'https://test/3/2', 'https://test/3/3'],
      ['https://test/4/1', 'https://test/4/2', 'https://test/4/3', 'https://test/4/4'],
      [
        'https://test/5/1',
        'https://test/5/2',
        'https://test/5/3',
        'https://test/5/4',
        'https://test/5/5'
      ]
    ]

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

    const effect = await Effect.all(sites.map(mapper2_gen({ concurrency: 2 })), {
      concurrency: Number.MAX_VALUE
    })
    const result = await Effect.runPromise(effect)

    console.log(result)
  })
})
```

実行結果
```ts
time : 2025-02-09T14:12:13.250Z - text: [start]
time : 2025-02-09T14:12:14.272Z - text: mapper2-index = 1
time : 2025-02-09T14:12:15.273Z - text: mapper2-index = 2
time : 2025-02-09T14:12:15.278Z - text: https://test/1/1
time : 2025-02-09T14:12:16.264Z - text: mapper2-index = 3
time : 2025-02-09T14:12:16.280Z - text: https://test/2/1
time : 2025-02-09T14:12:17.268Z - text: mapper2-index = 4
time : 2025-02-09T14:12:17.270Z - text: https://test/3/1
time : 2025-02-09T14:12:17.284Z - text: https://test/2/2
time : 2025-02-09T14:12:18.272Z - text: mapper2-index = 5
time : 2025-02-09T14:12:18.275Z - text: https://test/3/2
time : 2025-02-09T14:12:18.276Z - text: https://test/4/1
time : 2025-02-09T14:12:19.275Z - text: https://test/4/2
time : 2025-02-09T14:12:19.277Z - text: https://test/5/1
time : 2025-02-09T14:12:20.278Z - text: https://test/3/3
time : 2025-02-09T14:12:20.279Z - text: https://test/5/2
time : 2025-02-09T14:12:21.278Z - text: https://test/4/3
time : 2025-02-09T14:12:22.285Z - text: https://test/5/3
time : 2025-02-09T14:12:23.287Z - text: https://test/4/4
time : 2025-02-09T14:12:24.296Z - text: https://test/5/4
time : 2025-02-09T14:12:27.300Z - text: https://test/5/5
[
  'https://test/1 - 2000',
  'https://test/2 - 4000',
  'https://test/3 - 7000',
  'https://test/4 - 10000',
  'https://test/5 - 14000'
]
```