"'진행 중인 녹음 없음.' 녹음이 진행 중인 동안 앱이 한 말이야."
기능과 크래시
탭 문법은 손이 편하라고 만든 거야. 길게 누르면 눌러서 말하기, 짧게 톡 친 다음 한 번 더 치면 핸즈프리로 전환. 첫 구현은 뻔했고 틀렸어. 빠른 탭이 세션을 취소하고, 두 번째 탭이 새 핸즈프리를 시작하는 방식. 가끔 됐어. 가끔은 '진행 중인 녹음 없음' 으로 죽었고. 그 문장이 찍히는 바로 그 순간에 사실이 아니었던 에러 메시지지.
레이스
첫 탭이 async 취소를 쏴. 두 번째 탭이 새 세션을 시작하고. 취소가 느리면, 그리고 실제로 가끔 느려, 오디오를 무너뜨리는 게 즉각적이지 않으니까, 그 취소가 새 세션이 생긴 다음에 도착해. 자기가 낡았다는 걸 모른 채로 엉뚱한 녹음을 무너뜨리지. 이미 죽은 세션의 유령이 손을 뻗어서 살아 있는 걸 죽이는 거야. 그다음엔 세션이 있을 거라 믿은 코드가 아무것도 못 찾고, 그 어이없음을 그대로 신고하고.
탭 1 -> 세션 A 취소 (async, 느림...)
탭 2 -> 세션 B 시작 (빠름)
... A-취소가 드디어 착륙 ...
-> B 를 무너뜨림 # 유령이 엉뚱한 세션을 죽임
-> "진행 중인 녹음 없음"
해법 둘, 둘 다 필요해
첫 번째 해법이 레이스를 통째로 없애. 세션을 절대 재시작하지 않는다. 빠른 탭이 더는 취소를 안 해. 짧은 결정 창 동안, 그러니까 0.45초쯤 세션을 녹음 중인 채로 둬. 그 사이에 두 번째 탭이 오면 같은 세션이 그 자리에서 핸즈프리로 바뀌고. 두 번째 탭 없이 창이 만료되면 그때 취소해. 무너뜨리고 다시 시작하는 틈이 없으니 유령이 착륙할 자리도 없는 거야. 두 번째 해법은 심층 방어야. 오디오 세션마다 세대 토큰 을 찍고, 취소가 뭘 하기 전에 자기 세대부터 확인하게 해. 세대 1 의 취소가 세대 2 에 도착하면 아무 일도 안 하고 그냥 돌아가는 거야. no-op 이 되는 거지. 첫 번째 해법이 레이스 자체를 없애고, 두 번째 해법이 레이스가 다시 생겨도 피해를 못 주게 막아.