@cartanova/qgrid-cli 2.6.10 → 2.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +137 -60
  2. package/bundle/dist/application/qgrid/qgrid.frame.js +5 -2
  3. package/bundle/dist/application/setting/setting.model.js +65 -3
  4. package/bundle/dist/application/setting/setting.types.js +13 -3
  5. package/bundle/dist/i18n/en.js +3 -1
  6. package/bundle/dist/i18n/ko.js +3 -1
  7. package/bundle/dist/utils/process-supervisor.js +22 -0
  8. package/bundle/dist/utils/server-restart.js +61 -0
  9. package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +7 -0
  10. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +113 -0
  11. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +172 -75
  12. package/bundle/src/application/qgrid/qgrid.frame.test.ts +45 -0
  13. package/bundle/src/application/qgrid/qgrid.frame.ts +4 -0
  14. package/bundle/src/application/setting/setting.model.test.ts +104 -0
  15. package/bundle/src/application/setting/setting.model.ts +75 -3
  16. package/bundle/src/application/setting/setting.types.ts +12 -0
  17. package/bundle/src/application/sonamu.generated.http +7 -0
  18. package/bundle/src/i18n/en.ts +3 -0
  19. package/bundle/src/i18n/ko.ts +3 -0
  20. package/bundle/src/server-error-handler.test.ts +15 -0
  21. package/bundle/src/utils/process-supervisor.test.ts +24 -0
  22. package/bundle/src/utils/process-supervisor.ts +17 -0
  23. package/bundle/src/utils/server-restart.test.ts +76 -0
  24. package/bundle/src/utils/server-restart.ts +68 -0
  25. package/bundle/web-dist/client/assets/index-CFvFRDk_.js +78 -0
  26. package/bundle/web-dist/client/assets/index-CJ9iTI_V.css +1 -0
  27. package/bundle/web-dist/client/assets/{logs-BI5K8oe4.js → logs-DeAK5Uyu.js} +1 -1
  28. package/bundle/web-dist/client/assets/{monit-4Y-Razet.js → monit-Rj94Sz3_.js} +1 -1
  29. package/bundle/web-dist/client/assets/{routes-CkBXB-V4.js → routes-DVseTo6T.js} +1 -1
  30. package/bundle/web-dist/client/assets/{sd.generated-Bwa7rses.js → sd.generated-W6XqgWK6.js} +8 -8
  31. package/bundle/web-dist/client/assets/services.generated-Dz5pA5Fb.js +1 -0
  32. package/bundle/web-dist/client/assets/settings-DvuRDFfS.js +1 -0
  33. package/bundle/web-dist/client/assets/{show-CGRchJj7.js → show-C7etBj89.js} +1 -1
  34. package/bundle/web-dist/client/assets/{tokens-Bx5mQnoJ.js → tokens-BroPe7Tn.js} +1 -1
  35. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Bz87RV5v.js → use-oauth-login-flow-CuLvs_CW.js} +1 -1
  36. package/bundle/web-dist/client/index.html +3 -3
  37. package/bundle/web-dist/server/assets/{lazyRouteComponent-Ws7I6fL-.js → lazyRouteComponent-C-N6sd5N.js} +2 -2
  38. package/bundle/web-dist/server/assets/{logs-C1rxLh8H.js → logs-CVkeSr1S.js} +3 -3
  39. package/bundle/web-dist/server/assets/{monit-C8gx9Sar.js → monit-Bb6QfT9V.js} +3 -3
  40. package/bundle/web-dist/server/assets/{plus-BISXNqRU.js → plus-B8JRuCb9.js} +1 -1
  41. package/bundle/web-dist/server/assets/{react-dom-D7duWOwf.js → react-dom-ByF9tqTB.js} +1 -1
  42. package/bundle/web-dist/server/assets/{routes-ZW_Ih5Em.js → routes-Dnm-Eti6.js} +4 -4
  43. package/bundle/web-dist/server/assets/{sd.generated-LO-h1_16.js → sd.generated-CuyajYtc.js} +126 -116
  44. package/bundle/web-dist/server/assets/{services.generated-CZIChgHV.js → services.generated-BAGLi2r8.js} +9 -1
  45. package/bundle/web-dist/server/assets/{settings--aoatERH.js → settings-4T3nBfY8.js} +271 -39
  46. package/bundle/web-dist/server/assets/{show-BY27w9oi.js → show-CQw03iTP.js} +3 -3
  47. package/bundle/web-dist/server/assets/{tokens-DIObtTuZ.js → tokens-Di_SGPYj.js} +4 -4
  48. package/bundle/web-dist/server/assets/{use-oauth-login-flow-V3k8PSqQ.js → use-oauth-login-flow-DiYCMNAQ.js} +2 -2
  49. package/bundle/web-dist/server/entry-server.generated.js +10 -10
  50. package/dist/cli.js +93 -38
  51. package/package.json +1 -1
  52. package/skills/qgrid/references/cli-env-and-server-boot.md +34 -3
  53. package/bundle/web-dist/client/assets/index-BRRNJRgu.js +0 -78
  54. package/bundle/web-dist/client/assets/index-rdbxJinT.css +0 -1
  55. package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +0 -1
  56. package/bundle/web-dist/client/assets/settings-B62_r1i4.js +0 -1
@@ -48,8 +48,10 @@ var ko_default = {
48
48
  "error.serviceUnavailable": "서비스를 사용할 수 없습니다",
49
49
  "setting.invalidValue": (reason) => `설정 값이 올바르지 않습니다: ${reason}`,
50
50
  "setting.unknownKey": (key) => `알 수 없는 설정 키 ${key}`,
51
+ "setting.noSupervisor": () => "로컬 실행 환경에서는 재시작할 수 없습니다 — 종료하면 다시 켜줄 모니터링 도구가 없습니다",
51
52
  "qgrid.dispatcherFailed": (provider) => `${provider} dispatcher 초기화에 실패했습니다`,
52
53
  "qgrid.dispatcherStarting": (provider) => `${provider} dispatcher 기동 중입니다. 잠시 후 다시 시도해주세요`,
54
+ "qgrid.restartPending": () => "서버 재시작 중입니다. 잠시 후 다시 시도해주세요",
53
55
  "error.unauthorized": "인증이 필요합니다",
54
56
  "error.unknownSearchField": (field) => `구현되지 않은 검색 필드 ${field}`,
55
57
  notFound: (name, id) => `존재하지 않는 ${name} ID ${id}`,
@@ -62,4 +64,4 @@ var ko_default = {
62
64
  //#endregion
63
65
  export { ko_default as default };
64
66
 
65
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia28uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2kxOG4va28udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgam9zYSB9IGZyb20gXCJzb25hbXUvZGljdFwiO1xuXG4vKipcbiAqIFByb2plY3QgS08gRGljdGlvbmFyeVxuICovXG5leHBvcnQgZGVmYXVsdCB7XG4gIFwiY29tbW9uLmFsbFwiOiBcIuyghOyytFwiLFxuICBcImNvbW1vbi5iYWNrVG9MaXN0XCI6IFwi66qp66Gd7Jy866GcXCIsXG4gIFwiY29tbW9uLmNhbmNlbFwiOiBcIuy3qOyGjFwiLFxuICBcImNvbW1vbi5jbG9zZVwiOiBcIuuLq+q4sFwiLFxuICBcImNvbW1vbi5jb25maXJtXCI6IFwi7ZmV7J24XCIsXG4gIFwiY29tbW9uLmNyZWF0ZVwiOiBcIuyDneyEsVwiLFxuICBcImNvbW1vbi5jcmVhdGVkQXRcIjogXCLrk7HroZ1cIixcbiAgXCJjb21tb24uZGVsZXRlXCI6IFwi7IKt7KCcXCIsXG4gIFwiY29tbW9uLmVkaXRcIjogXCLsiJjsoJVcIixcbiAgXCJjb21tb24ubG9naW5cIjogXCLroZzqt7jsnbhcIixcbiAgXCJjb21tb24ubG9nb3V0XCI6IFwi66Gc6re47JWE7JuDXCIsXG4gIFwiY29tbW9uLm1hbmFnZVwiOiBcIuq0gOumrFwiLFxuICBcImNvbW1vbi5yZXN1bHRzXCI6IChjb3VudDogbnVtYmVyKSA9PiBgJHtjb3VudH3qsJwg6rKw6rO8YCxcbiAgXCJjb21tb24uc2F2ZVwiOiBcIuyggOyepVwiLFxuICBcImNvbW1vbi5zZWFyY2hcIjogXCLqsoDsg4lcIixcbiAgXCJjb21tb24uc2VhcmNoUGxhY2Vob2xkZXJcIjogXCLqsoDsg4kuLi5cIixcbiAgXCJjb21tb24uc2VhcmNoVHlwZVwiOiBcIuqygOyDiSDsnKDtmJVcIixcbiAgXCJjb21tb24uc29ydFwiOiBcIuygleugrFwiLFxuICBcImNvbmZpcm0uZGVsZXRlXCI6IFwi7KCV66eQIOyCreygnO2VmOyLnOqyoOyKteuLiOq5jD9cIixcbiAgXCJjb25maXJtLnNhdmVcIjogXCLsoIDsnqXtlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZGFzaGJvYXJkLmFkbWluTWVudVwiOiBcIuq0gOumrCDrqZTribRcIixcbiAgXCJkYXNoYm9hcmQuY3JlYXRlZEF0XCI6IFwi6rCA7J6F7J28XCIsXG4gIFwiZGFzaGJvYXJkLmVtYWlsXCI6IFwi7J2066mU7J28XCIsXG4gIFwiZGFzaGJvYXJkLmxvZ2luUmVxdWlyZWRcIjogXCLroZzqt7jsnbjsnbQg7ZWE7JqU7ZWp64uI64ukLlwiLFxuICBcImRhc2hib2FyZC5uYW1lXCI6IFwi7J2066aEXCIsXG4gIFwiZGFzaGJvYXJkLnJvbGVcIjogXCLsl63tlaBcIixcbiAgXCJkYXNoYm9hcmQudGl0bGVcIjogXCLqtIDrpqzsnpAg64yA7Iuc67O065OcXCIsXG4gIFwiZGFzaGJvYXJkLndlbGNvbWVcIjogXCLtmZjsmIHtlanri4jri6QhXCIsXG4gIFwiZGVsZXRlLmNvbmZpcm0uZGVzY3JpcHRpb25cIjogXCLsnbQg7J6R7JeF7J2AIOy3qOyGjO2VoCDsiJgg7JeG7Iq164uI64ukLiDtla3rqqnsnbQg7JiB6rWs7KCB7Jy866GcIOyCreygnOuQqeuLiOuLpC5cIixcbiAgXCJkZWxldGUuY29uZmlybS50aXRsZVwiOiBcIuygleunkCDsgq3soJztlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZW50aXR5LmNyZWF0ZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDsg53shLFgLFxuICBcImVudGl0eS5lZGl0XCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIpID0+IGAke25hbWV9IOyImOyglSAoIyR7aWR9KWAsXG4gIFwiZW50aXR5Lmxpc3RcIjogKG5hbWU6IHN0cmluZykgPT4gYCR7bmFtZX0g66qp66GdYCxcbiAgXCJlbnRpdHkubGlzdE1hbmFnZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDrqqnroZ0g6rSA66asYCxcbiAgXCJlcnJvci5hbHJlYWR5UHJvY2Vzc2VkXCI6IFwi7J2066+4IOyymOumrOuQmOyXiOyKteuLiOuLpFwiLFxuICBcImVycm9yLmJhZFJlcXVlc3RcIjogXCLsnpjrqrvrkJwg7JqU7LKt7J6F64uI64ukXCIsXG4gIFwiZXJyb3IuZHVwbGljYXRlUm93XCI6IFwi7KSR67O165CcIOuNsOydtO2EsOyeheuLiOuLpFwiLFxuICBcImVycm9yLmVudGl0eU5vdEZvdW5kXCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIgfCBzdHJpbmcpID0+IGDsobTsnqztlZjsp4Ag7JWK64qUICR7bmFtZX0gSUQgJHtpZH1gLFxuICBcImVycm9yLmZvcmJpZGRlblwiOiBcIuq2jO2VnOydtCDsl4bsirXri4jri6RcIixcbiAgXCJlcnJvci5pbnRlcm5hbFNlcnZlckVycm9yXCI6IFwi7ISc67KEIOyYpOulmOqwgCDrsJzsg53tlojsirXri4jri6RcIixcbiAgXCJlcnJvci5ub3RGb3VuZFwiOiBcIuywvuydhCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwiZXJyb3Iuc2VydmljZVVuYXZhaWxhYmxlXCI6IFwi7ISc67mE7Iqk66W8IOyCrOyaqe2VoCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwic2V0dGluZy5pbnZhbGlkVmFsdWVcIjogKHJlYXNvbjogc3RyaW5nKSA9PiBg7ISk7KCVIOqwkuydtCDsmKzrsJTrpbTsp4Ag7JWK7Iq164uI64ukOiAke3JlYXNvbn1gLFxuICBcInNldHRpbmcudW5rbm93bktleVwiOiAoa2V5OiBzdHJpbmcpID0+IGDslYwg7IiYIOyXhuuKlCDshKTsoJUg7YKkICR7a2V5fWAsXG4gIFwicWdyaWQuZGlzcGF0Y2hlckZhaWxlZFwiOiAocHJvdmlkZXI6IHN0cmluZykgPT4gYCR7cHJvdmlkZXJ9IGRpc3BhdGNoZXIg7LSI6riw7ZmU7JeQIOyLpO2MqO2WiOyKteuLiOuLpGAsXG4gIFwicWdyaWQuZGlzcGF0Y2hlclN0YXJ0aW5nXCI6IChwcm92aWRlcjogc3RyaW5nKSA9PlxuICAgIGAke3Byb3ZpZGVyfSBkaXNwYXRjaGVyIOq4sOuPmSDspJHsnoXri4jri6QuIOyeoOyLnCDtm4Qg64uk7IucIOyLnOuPhO2VtOyjvOyEuOyalGAsXG4gIFwiZXJyb3IudW5hdXRob3JpemVkXCI6IFwi7J247Kad7J20IO2VhOyalO2VqeuLiOuLpFwiLFxuICBcImVycm9yLnVua25vd25TZWFyY2hGaWVsZFwiOiAoZmllbGQ6IHN0cmluZykgPT4gYOq1rO2YhOuQmOyngCDslYrsnYAg6rKA7IOJIO2VhOuTnCAke2ZpZWxkfWAsXG4gIG5vdEZvdW5kOiAobmFtZTogc3RyaW5nLCBpZDogbnVtYmVyKSA9PiBg7KG07J6s7ZWY7KeAIOyViuuKlCAke25hbWV9IElEICR7aWR9YCxcbiAgXCJ2YWxpZGF0aW9uLmVtYWlsXCI6IFwi7Jis67CU66W4IOydtOuplOydvCDtmJXsi53snbQg7JWE64uZ64uI64ukXCIsXG4gIFwidmFsaWRhdGlvbi5tYXhMZW5ndGhcIjogKGZpZWxkOiBzdHJpbmcsIG1heDogbnVtYmVyKSA9PlxuICAgIGAke2ZpZWxkfSR7am9zYShmaWVsZCwgXCLsnYDripRcIil9IOy1nOuMgCAke21heH3snpDquYzsp4Ag7J6F66Cl7ZWgIOyImCDsnojsirXri4jri6RgLFxuICBcInZhbGlkYXRpb24ubWluTGVuZ3RoXCI6IChmaWVsZDogc3RyaW5nLCBtaW46IG51bWJlcikgPT5cbiAgICBgJHtmaWVsZH0ke2pvc2EoZmllbGQsIFwi7J2A64qUXCIpfSDstZzshowgJHttaW597J6QIOydtOyDgeydtOyWtOyVvCDtlanri4jri6RgLFxuICBcInZhbGlkYXRpb24ucmVxdWlyZWRcIjogKGZpZWxkOiBzdHJpbmcpID0+IGAke2pvc2EoZmllbGQsIFwi7J2A64qUXCIpfSDtlYTsiJjsnoXri4jri6RgLFxuICBcInZhbGlkYXRpb24udXJsXCI6IFwi7Jis67CU66W4IFVSTCDtmJXsi53snbQg7JWE64uZ64uI64ukXCIsXG59IGFzIGNvbnN0O1xuIl0sIm1hcHBpbmdzIjoiOzs7OztBQUtBLElBQUEsYUFBZTtDQUNiLGNBQWM7Q0FDZCxxQkFBcUI7Q0FDckIsaUJBQWlCO0NBQ2pCLGdCQUFnQjtDQUNoQixrQkFBa0I7Q0FDbEIsaUJBQWlCO0NBQ2pCLG9CQUFvQjtDQUNwQixpQkFBaUI7Q0FDakIsZUFBZTtDQUNmLGdCQUFnQjtDQUNoQixpQkFBaUI7Q0FDakIsaUJBQWlCO0NBQ2pCLG1CQUFtQixVQUFrQixHQUFHLE1BQU07Q0FDOUMsZUFBZTtDQUNmLGlCQUFpQjtDQUNqQiw0QkFBNEI7Q0FDNUIscUJBQXFCO0NBQ3JCLGVBQWU7Q0FDZixrQkFBa0I7Q0FDbEIsZ0JBQWdCO0NBQ2hCLHVCQUF1QjtDQUN2Qix1QkFBdUI7Q0FDdkIsbUJBQW1CO0NBQ25CLDJCQUEyQjtDQUMzQixrQkFBa0I7Q0FDbEIsa0JBQWtCO0NBQ2xCLG1CQUFtQjtDQUNuQixxQkFBcUI7Q0FDckIsOEJBQThCO0NBQzlCLHdCQUF3QjtDQUN4QixrQkFBa0IsU0FBaUIsR0FBRyxLQUFLO0NBQzNDLGdCQUFnQixNQUFjLE9BQWUsR0FBRyxLQUFLLFFBQVEsR0FBRztDQUNoRSxnQkFBZ0IsU0FBaUIsR0FBRyxLQUFLO0NBQ3pDLHNCQUFzQixTQUFpQixHQUFHLEtBQUs7Q0FDL0MsMEJBQTBCO0NBQzFCLG9CQUFvQjtDQUNwQixzQkFBc0I7Q0FDdEIseUJBQXlCLE1BQWMsT0FBd0IsV0FBVyxLQUFLLE1BQU07Q0FDckYsbUJBQW1CO0NBQ25CLDZCQUE2QjtDQUM3QixrQkFBa0I7Q0FDbEIsNEJBQTRCO0NBQzVCLHlCQUF5QixXQUFtQixvQkFBb0I7Q0FDaEUsdUJBQXVCLFFBQWdCLGVBQWU7Q0FDdEQsMkJBQTJCLGFBQXFCLEdBQUcsU0FBUztDQUM1RCw2QkFBNkIsYUFDM0IsR0FBRyxTQUFTO0NBQ2Qsc0JBQXNCO0NBQ3RCLDZCQUE2QixVQUFrQixpQkFBaUI7Q0FDaEUsV0FBVyxNQUFjLE9BQWUsV0FBVyxLQUFLLE1BQU07Q0FDOUQsb0JBQW9CO0NBQ3BCLHlCQUF5QixPQUFlLFFBQ3RDLEdBQUcsUUFBUSxLQUFLLE9BQU8sSUFBSSxFQUFFLE1BQU0sSUFBSTtDQUN6Qyx5QkFBeUIsT0FBZSxRQUN0QyxHQUFHLFFBQVEsS0FBSyxPQUFPLElBQUksRUFBRSxNQUFNLElBQUk7Q0FDekMsd0JBQXdCLFVBQWtCLEdBQUcsS0FBSyxPQUFPLElBQUksRUFBRTtDQUMvRCxrQkFBa0I7QUFDcEIifQ==
67
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia28uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2kxOG4va28udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgam9zYSB9IGZyb20gXCJzb25hbXUvZGljdFwiO1xuXG4vKipcbiAqIFByb2plY3QgS08gRGljdGlvbmFyeVxuICovXG5leHBvcnQgZGVmYXVsdCB7XG4gIFwiY29tbW9uLmFsbFwiOiBcIuyghOyytFwiLFxuICBcImNvbW1vbi5iYWNrVG9MaXN0XCI6IFwi66qp66Gd7Jy866GcXCIsXG4gIFwiY29tbW9uLmNhbmNlbFwiOiBcIuy3qOyGjFwiLFxuICBcImNvbW1vbi5jbG9zZVwiOiBcIuuLq+q4sFwiLFxuICBcImNvbW1vbi5jb25maXJtXCI6IFwi7ZmV7J24XCIsXG4gIFwiY29tbW9uLmNyZWF0ZVwiOiBcIuyDneyEsVwiLFxuICBcImNvbW1vbi5jcmVhdGVkQXRcIjogXCLrk7HroZ1cIixcbiAgXCJjb21tb24uZGVsZXRlXCI6IFwi7IKt7KCcXCIsXG4gIFwiY29tbW9uLmVkaXRcIjogXCLsiJjsoJVcIixcbiAgXCJjb21tb24ubG9naW5cIjogXCLroZzqt7jsnbhcIixcbiAgXCJjb21tb24ubG9nb3V0XCI6IFwi66Gc6re47JWE7JuDXCIsXG4gIFwiY29tbW9uLm1hbmFnZVwiOiBcIuq0gOumrFwiLFxuICBcImNvbW1vbi5yZXN1bHRzXCI6IChjb3VudDogbnVtYmVyKSA9PiBgJHtjb3VudH3qsJwg6rKw6rO8YCxcbiAgXCJjb21tb24uc2F2ZVwiOiBcIuyggOyepVwiLFxuICBcImNvbW1vbi5zZWFyY2hcIjogXCLqsoDsg4lcIixcbiAgXCJjb21tb24uc2VhcmNoUGxhY2Vob2xkZXJcIjogXCLqsoDsg4kuLi5cIixcbiAgXCJjb21tb24uc2VhcmNoVHlwZVwiOiBcIuqygOyDiSDsnKDtmJVcIixcbiAgXCJjb21tb24uc29ydFwiOiBcIuygleugrFwiLFxuICBcImNvbmZpcm0uZGVsZXRlXCI6IFwi7KCV66eQIOyCreygnO2VmOyLnOqyoOyKteuLiOq5jD9cIixcbiAgXCJjb25maXJtLnNhdmVcIjogXCLsoIDsnqXtlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZGFzaGJvYXJkLmFkbWluTWVudVwiOiBcIuq0gOumrCDrqZTribRcIixcbiAgXCJkYXNoYm9hcmQuY3JlYXRlZEF0XCI6IFwi6rCA7J6F7J28XCIsXG4gIFwiZGFzaGJvYXJkLmVtYWlsXCI6IFwi7J2066mU7J28XCIsXG4gIFwiZGFzaGJvYXJkLmxvZ2luUmVxdWlyZWRcIjogXCLroZzqt7jsnbjsnbQg7ZWE7JqU7ZWp64uI64ukLlwiLFxuICBcImRhc2hib2FyZC5uYW1lXCI6IFwi7J2066aEXCIsXG4gIFwiZGFzaGJvYXJkLnJvbGVcIjogXCLsl63tlaBcIixcbiAgXCJkYXNoYm9hcmQudGl0bGVcIjogXCLqtIDrpqzsnpAg64yA7Iuc67O065OcXCIsXG4gIFwiZGFzaGJvYXJkLndlbGNvbWVcIjogXCLtmZjsmIHtlanri4jri6QhXCIsXG4gIFwiZGVsZXRlLmNvbmZpcm0uZGVzY3JpcHRpb25cIjogXCLsnbQg7J6R7JeF7J2AIOy3qOyGjO2VoCDsiJgg7JeG7Iq164uI64ukLiDtla3rqqnsnbQg7JiB6rWs7KCB7Jy866GcIOyCreygnOuQqeuLiOuLpC5cIixcbiAgXCJkZWxldGUuY29uZmlybS50aXRsZVwiOiBcIuygleunkCDsgq3soJztlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZW50aXR5LmNyZWF0ZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDsg53shLFgLFxuICBcImVudGl0eS5lZGl0XCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIpID0+IGAke25hbWV9IOyImOyglSAoIyR7aWR9KWAsXG4gIFwiZW50aXR5Lmxpc3RcIjogKG5hbWU6IHN0cmluZykgPT4gYCR7bmFtZX0g66qp66GdYCxcbiAgXCJlbnRpdHkubGlzdE1hbmFnZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDrqqnroZ0g6rSA66asYCxcbiAgXCJlcnJvci5hbHJlYWR5UHJvY2Vzc2VkXCI6IFwi7J2066+4IOyymOumrOuQmOyXiOyKteuLiOuLpFwiLFxuICBcImVycm9yLmJhZFJlcXVlc3RcIjogXCLsnpjrqrvrkJwg7JqU7LKt7J6F64uI64ukXCIsXG4gIFwiZXJyb3IuZHVwbGljYXRlUm93XCI6IFwi7KSR67O165CcIOuNsOydtO2EsOyeheuLiOuLpFwiLFxuICBcImVycm9yLmVudGl0eU5vdEZvdW5kXCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIgfCBzdHJpbmcpID0+IGDsobTsnqztlZjsp4Ag7JWK64qUICR7bmFtZX0gSUQgJHtpZH1gLFxuICBcImVycm9yLmZvcmJpZGRlblwiOiBcIuq2jO2VnOydtCDsl4bsirXri4jri6RcIixcbiAgXCJlcnJvci5pbnRlcm5hbFNlcnZlckVycm9yXCI6IFwi7ISc67KEIOyYpOulmOqwgCDrsJzsg53tlojsirXri4jri6RcIixcbiAgXCJlcnJvci5ub3RGb3VuZFwiOiBcIuywvuydhCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwiZXJyb3Iuc2VydmljZVVuYXZhaWxhYmxlXCI6IFwi7ISc67mE7Iqk66W8IOyCrOyaqe2VoCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwic2V0dGluZy5pbnZhbGlkVmFsdWVcIjogKHJlYXNvbjogc3RyaW5nKSA9PiBg7ISk7KCVIOqwkuydtCDsmKzrsJTrpbTsp4Ag7JWK7Iq164uI64ukOiAke3JlYXNvbn1gLFxuICBcInNldHRpbmcudW5rbm93bktleVwiOiAoa2V5OiBzdHJpbmcpID0+IGDslYwg7IiYIOyXhuuKlCDshKTsoJUg7YKkICR7a2V5fWAsXG4gIFwic2V0dGluZy5ub1N1cGVydmlzb3JcIjogKCkgPT5cbiAgICBcIuuhnOy7rCDsi6Ttlokg7ZmY6rK97JeQ7ISc64qUIOyerOyLnOyeke2VoCDsiJgg7JeG7Iq164uI64ukIOKAlCDsooXro4ztlZjrqbQg64uk7IucIOy8nOykhCDrqqjri4jthLDrp4Eg64+E6rWs6rCAIOyXhuyKteuLiOuLpFwiLFxuICBcInFncmlkLmRpc3BhdGNoZXJGYWlsZWRcIjogKHByb3ZpZGVyOiBzdHJpbmcpID0+IGAke3Byb3ZpZGVyfSBkaXNwYXRjaGVyIOy0iOq4sO2ZlOyXkCDsi6TtjKjtlojsirXri4jri6RgLFxuICBcInFncmlkLmRpc3BhdGNoZXJTdGFydGluZ1wiOiAocHJvdmlkZXI6IHN0cmluZykgPT5cbiAgICBgJHtwcm92aWRlcn0gZGlzcGF0Y2hlciDquLDrj5kg7KSR7J6F64uI64ukLiDsnqDsi5wg7ZuEIOuLpOyLnCDsi5zrj4TtlbTso7zshLjsmpRgLFxuICBcInFncmlkLnJlc3RhcnRQZW5kaW5nXCI6ICgpID0+IFwi7ISc67KEIOyerOyLnOyekSDspJHsnoXri4jri6QuIOyeoOyLnCDtm4Qg64uk7IucIOyLnOuPhO2VtOyjvOyEuOyalFwiLFxuICBcImVycm9yLnVuYXV0aG9yaXplZFwiOiBcIuyduOymneydtCDtlYTsmpTtlanri4jri6RcIixcbiAgXCJlcnJvci51bmtub3duU2VhcmNoRmllbGRcIjogKGZpZWxkOiBzdHJpbmcpID0+IGDqtaztmITrkJjsp4Ag7JWK7J2AIOqygOyDiSDtlYTrk5wgJHtmaWVsZH1gLFxuICBub3RGb3VuZDogKG5hbWU6IHN0cmluZywgaWQ6IG51bWJlcikgPT4gYOyhtOyerO2VmOyngCDslYrripQgJHtuYW1lfSBJRCAke2lkfWAsXG4gIFwidmFsaWRhdGlvbi5lbWFpbFwiOiBcIuyYrOuwlOuluCDsnbTrqZTsnbwg7ZiV7Iud7J20IOyVhOuLmeuLiOuLpFwiLFxuICBcInZhbGlkYXRpb24ubWF4TGVuZ3RoXCI6IChmaWVsZDogc3RyaW5nLCBtYXg6IG51bWJlcikgPT5cbiAgICBgJHtmaWVsZH0ke2pvc2EoZmllbGQsIFwi7J2A64qUXCIpfSDstZzrjIAgJHttYXh97J6Q6rmM7KeAIOyeheugpe2VoCDsiJgg7J6I7Iq164uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLm1pbkxlbmd0aFwiOiAoZmllbGQ6IHN0cmluZywgbWluOiBudW1iZXIpID0+XG4gICAgYCR7ZmllbGR9JHtqb3NhKGZpZWxkLCBcIuydgOuKlFwiKX0g7LWc7IaMICR7bWlufeyekCDsnbTsg4HsnbTslrTslbwg7ZWp64uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLnJlcXVpcmVkXCI6IChmaWVsZDogc3RyaW5nKSA9PiBgJHtqb3NhKGZpZWxkLCBcIuydgOuKlFwiKX0g7ZWE7IiY7J6F64uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLnVybFwiOiBcIuyYrOuwlOuluCBVUkwg7ZiV7Iud7J20IOyVhOuLmeuLiOuLpFwiLFxufSBhcyBjb25zdDtcbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFLQSxJQUFBLGFBQWU7Q0FDYixjQUFjO0NBQ2QscUJBQXFCO0NBQ3JCLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsa0JBQWtCO0NBQ2xCLGlCQUFpQjtDQUNqQixvQkFBb0I7Q0FDcEIsaUJBQWlCO0NBQ2pCLGVBQWU7Q0FDZixnQkFBZ0I7Q0FDaEIsaUJBQWlCO0NBQ2pCLGlCQUFpQjtDQUNqQixtQkFBbUIsVUFBa0IsR0FBRyxNQUFNO0NBQzlDLGVBQWU7Q0FDZixpQkFBaUI7Q0FDakIsNEJBQTRCO0NBQzVCLHFCQUFxQjtDQUNyQixlQUFlO0NBQ2Ysa0JBQWtCO0NBQ2xCLGdCQUFnQjtDQUNoQix1QkFBdUI7Q0FDdkIsdUJBQXVCO0NBQ3ZCLG1CQUFtQjtDQUNuQiwyQkFBMkI7Q0FDM0Isa0JBQWtCO0NBQ2xCLGtCQUFrQjtDQUNsQixtQkFBbUI7Q0FDbkIscUJBQXFCO0NBQ3JCLDhCQUE4QjtDQUM5Qix3QkFBd0I7Q0FDeEIsa0JBQWtCLFNBQWlCLEdBQUcsS0FBSztDQUMzQyxnQkFBZ0IsTUFBYyxPQUFlLEdBQUcsS0FBSyxRQUFRLEdBQUc7Q0FDaEUsZ0JBQWdCLFNBQWlCLEdBQUcsS0FBSztDQUN6QyxzQkFBc0IsU0FBaUIsR0FBRyxLQUFLO0NBQy9DLDBCQUEwQjtDQUMxQixvQkFBb0I7Q0FDcEIsc0JBQXNCO0NBQ3RCLHlCQUF5QixNQUFjLE9BQXdCLFdBQVcsS0FBSyxNQUFNO0NBQ3JGLG1CQUFtQjtDQUNuQiw2QkFBNkI7Q0FDN0Isa0JBQWtCO0NBQ2xCLDRCQUE0QjtDQUM1Qix5QkFBeUIsV0FBbUIsb0JBQW9CO0NBQ2hFLHVCQUF1QixRQUFnQixlQUFlO0NBQ3RELDhCQUNFO0NBQ0YsMkJBQTJCLGFBQXFCLEdBQUcsU0FBUztDQUM1RCw2QkFBNkIsYUFDM0IsR0FBRyxTQUFTO0NBQ2QsOEJBQThCO0NBQzlCLHNCQUFzQjtDQUN0Qiw2QkFBNkIsVUFBa0IsaUJBQWlCO0NBQ2hFLFdBQVcsTUFBYyxPQUFlLFdBQVcsS0FBSyxNQUFNO0NBQzlELG9CQUFvQjtDQUNwQix5QkFBeUIsT0FBZSxRQUN0QyxHQUFHLFFBQVEsS0FBSyxPQUFPLElBQUksRUFBRSxNQUFNLElBQUk7Q0FDekMseUJBQXlCLE9BQWUsUUFDdEMsR0FBRyxRQUFRLEtBQUssT0FBTyxJQUFJLEVBQUUsTUFBTSxJQUFJO0NBQ3pDLHdCQUF3QixVQUFrQixHQUFHLEtBQUssT0FBTyxJQUFJLEVBQUU7Q0FDL0Qsa0JBQWtCO0FBQ3BCIn0=
@@ -0,0 +1,22 @@
1
+ //#region src/utils/process-supervisor.ts
2
+ /**
3
+ * 프로세스 모니터링 도구 감지.
4
+ *
5
+ * 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
6
+ * 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
7
+ * 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
8
+ *
9
+ * 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
10
+ * unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
11
+ *
12
+ * 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
13
+ * 판별하면 놓친다.
14
+ */
15
+ function detectSupervisor(env = process.env) {
16
+ if (env.pm_id !== void 0) return "pm2";
17
+ return null;
18
+ }
19
+ //#endregion
20
+ export { detectSupervisor };
21
+
22
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1zdXBlcnZpc29yLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9wcm9jZXNzLXN1cGVydmlzb3IudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDtlITroZzshLjsiqQg66qo64uI7YSw66eBIOuPhOq1rCDqsJDsp4AuXG4gKlxuICog7ZmU66m07J2YIFwi7J6s7Iuc7J6RXCLsnYAg7Iqk7Iqk66Gc66W8IOyiheujjO2VmOuKlCDqsoPsnbTri6Qg4oCUIHBtMiDqsJnsnYAg64+E6rWs6rCAIOyjveydgCDtlITroZzshLjsiqTrpbwg64uk7IucXG4gKiDrnYTsm4zso7zripQg6rKD7J20IOqzpyDsnqzsi5zsnpHsnbTri6QuIOq3uOuemOyEnCDqt7jrn7Ag64+E6rWsIOyXhuydtCDsooXro4ztlZjrqbQg6re464OlIOyEnOuyhOqwgCDrgrTroKTqsITri6QuXG4gKiDroZzsu6wg6rCc67Cc7JeQ7IScIOuyhO2KvOydhCDriITrpbTrqbQg7JWE66y064+EIOyCtOugpOyjvOyngCDslYrsnLzrr4DroZwsIOyiheujjCDsoITsl5Ag67CY65Oc7IucIO2ZleyduO2VnOuLpC5cbiAqXG4gKiDtmITsnqwg67Cw7Y+sIOqzhOyVveydgCBwbTIg66eMIOyerOyLnOyekeydhCDrs7TsnqXtlZzri6QuIHN5c3RlbWQg7J2YIGBJTlZPQ0FUSU9OX0lEYCDripQg7ZW064u5XG4gKiB1bml0IOydtCBgUmVzdGFydD1gIOulvCDsk7Tri6TripQg65y77J20IOyVhOuLiOuvgOuhnCDslYjsoITtlZwg6rCQ64+F7J6QIOyLoO2YuOuhnCDsgqzsmqntlaAg7IiYIOyXhuuLpC5cbiAqXG4gKiDqsJLsnbQg7JWE64uI6528IOyhtOyerCDsl6zrtoDqsIAg7Iug7Zi464ukLiBwbTIg64qUIOyyqyDtlITroZzshLjsiqTsl5AgYHBtX2lkPTBgIOydhCDso7zrr4DroZwgdHJ1dGh5IOqygOyCrOuhnFxuICog7YyQ67OE7ZWY66m0IOuGk+y5nOuLpC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGRldGVjdFN1cGVydmlzb3IoZW52OiBOb2RlSlMuUHJvY2Vzc0VudiA9IHByb2Nlc3MuZW52KTogXCJwbTJcIiB8IG51bGwge1xuICBpZiAoZW52LnBtX2lkICE9PSB1bmRlZmluZWQpIHJldHVybiBcInBtMlwiO1xuICByZXR1cm4gbnVsbDtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7QUFhQSxTQUFnQixpQkFBaUIsTUFBeUIsUUFBUSxLQUFtQjtDQUNuRixJQUFJLElBQUksVUFBVSxRQUFXLE9BQU87Q0FDcEMsT0FBTztBQUNUIn0=
@@ -0,0 +1,61 @@
1
+ //#region src/utils/server-restart.ts
2
+ const RESTART_EXIT_FALLBACK_MS = 5e3;
3
+ let restartPending = false;
4
+ let exitTriggered = false;
5
+ let exitArmed = false;
6
+ let fallbackTimer;
7
+ let armedReplyRaw;
8
+ let armedExit;
9
+ /** 재시작 요청 뒤에는 새 native provider 실행을 받지 않는다. */
10
+ function isRestartPending() {
11
+ return restartPending;
12
+ }
13
+ /**
14
+ * 프로세스 전체에서 최초 요청만 재시작을 시작한다. 종료 신호는 active run 정합성 처리가
15
+ * 끝난 뒤 별도로 arm 한다. 그래야 이미 닫힌 연결이나 fallback 타이머가 DB 정리보다 앞서
16
+ * 프로세스를 끝내지 않는다.
17
+ */
18
+ function beginServerRestart() {
19
+ if (restartPending) return false;
20
+ restartPending = true;
21
+ return true;
22
+ }
23
+ function clearExitArm() {
24
+ if (fallbackTimer) clearTimeout(fallbackTimer);
25
+ fallbackTimer = void 0;
26
+ if (armedReplyRaw && armedExit) {
27
+ armedReplyRaw.removeListener("finish", armedExit);
28
+ armedReplyRaw.removeListener("close", armedExit);
29
+ }
30
+ armedReplyRaw = void 0;
31
+ armedExit = void 0;
32
+ }
33
+ /** 정합성 처리가 끝난 뒤 HTTP 응답 flush/연결 종료/유한 fallback 중 먼저 온 신호로 종료한다. */
34
+ function armServerRestartExit(replyRaw) {
35
+ if (!restartPending || exitArmed) return;
36
+ exitArmed = true;
37
+ const exitOnce = () => {
38
+ if (exitTriggered) return;
39
+ exitTriggered = true;
40
+ clearExitArm();
41
+ process.exit(0);
42
+ };
43
+ armedReplyRaw = replyRaw;
44
+ armedExit = exitOnce;
45
+ replyRaw.once("finish", exitOnce);
46
+ replyRaw.once("close", exitOnce);
47
+ fallbackTimer = setTimeout(exitOnce, RESTART_EXIT_FALLBACK_MS);
48
+ fallbackTimer.unref();
49
+ if (replyRaw.destroyed && !replyRaw.writableEnded) exitOnce();
50
+ }
51
+ /** 모듈 전역 상태를 격리해야 하는 단위 테스트 전용. */
52
+ function resetServerRestartForTests() {
53
+ clearExitArm();
54
+ restartPending = false;
55
+ exitTriggered = false;
56
+ exitArmed = false;
57
+ }
58
+ //#endregion
59
+ export { armServerRestartExit, beginServerRestart, isRestartPending, resetServerRestartForTests };
60
+
61
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXJlc3RhcnQuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3NlcnZlci1yZXN0YXJ0LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHR5cGUgU2VydmVyUmVzcG9uc2UgfSBmcm9tIFwibm9kZTpodHRwXCI7XG5cbmNvbnN0IFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyA9IDVfMDAwO1xuXG5sZXQgcmVzdGFydFBlbmRpbmcgPSBmYWxzZTtcbmxldCBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG5sZXQgZXhpdEFybWVkID0gZmFsc2U7XG5sZXQgZmFsbGJhY2tUaW1lcjogTm9kZUpTLlRpbWVvdXQgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRSZXBseVJhdzogUmVwbHlSYXcgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRFeGl0OiAoKCkgPT4gdm9pZCkgfCB1bmRlZmluZWQ7XG5cbnR5cGUgUmVwbHlSYXcgPSBQaWNrPFNlcnZlclJlc3BvbnNlLCBcIm9uY2VcIiB8IFwicmVtb3ZlTGlzdGVuZXJcIiB8IFwiZGVzdHJveWVkXCIgfCBcIndyaXRhYmxlRW5kZWRcIj47XG5cbi8qKiDsnqzsi5zsnpEg7JqU7LKtIOuSpOyXkOuKlCDsg4ggbmF0aXZlIHByb3ZpZGVyIOyLpO2WieydhCDrsJvsp4Ag7JWK64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUmVzdGFydFBlbmRpbmcoKTogYm9vbGVhbiB7XG4gIHJldHVybiByZXN0YXJ0UGVuZGluZztcbn1cblxuLyoqXG4gKiDtlITroZzshLjsiqQg7KCE7LK07JeQ7IScIOy1nOy0iCDsmpTssq3rp4wg7J6s7Iuc7J6R7J2EIOyLnOyeke2VnOuLpC4g7KKF66OMIOyLoO2YuOuKlCBhY3RpdmUgcnVuIOygle2VqeyEsSDsspjrpqzqsIBcbiAqIOuBneuCnCDrkqQg67OE64+E66GcIGFybSDtlZzri6QuIOq3uOuemOyVvCDsnbTrr7gg64ur7Z6MIOyXsOqysOydtOuCmCBmYWxsYmFjayDtg4DsnbTrqLjqsIAgREIg7KCV66as67O064ukIOyVnuyEnFxuICog7ZSE66Gc7IS47Iqk66W8IOuBneuCtOyngCDslYrripTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBiZWdpblNlcnZlclJlc3RhcnQoKTogYm9vbGVhbiB7XG4gIGlmIChyZXN0YXJ0UGVuZGluZykgcmV0dXJuIGZhbHNlO1xuICByZXN0YXJ0UGVuZGluZyA9IHRydWU7XG4gIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBjbGVhckV4aXRBcm0oKTogdm9pZCB7XG4gIGlmIChmYWxsYmFja1RpbWVyKSBjbGVhclRpbWVvdXQoZmFsbGJhY2tUaW1lcik7XG4gIGZhbGxiYWNrVGltZXIgPSB1bmRlZmluZWQ7XG4gIGlmIChhcm1lZFJlcGx5UmF3ICYmIGFybWVkRXhpdCkge1xuICAgIGFybWVkUmVwbHlSYXcucmVtb3ZlTGlzdGVuZXIoXCJmaW5pc2hcIiwgYXJtZWRFeGl0KTtcbiAgICBhcm1lZFJlcGx5UmF3LnJlbW92ZUxpc3RlbmVyKFwiY2xvc2VcIiwgYXJtZWRFeGl0KTtcbiAgfVxuICBhcm1lZFJlcGx5UmF3ID0gdW5kZWZpbmVkO1xuICBhcm1lZEV4aXQgPSB1bmRlZmluZWQ7XG59XG5cbi8qKiDsoJXtlanshLEg7LKY66as6rCAIOuBneuCnCDrkqQgSFRUUCDsnZHri7UgZmx1c2gv7Jew6rKwIOyiheujjC/snKDtlZwgZmFsbGJhY2sg7KSRIOuovOyggCDsmKgg7Iug7Zi466GcIOyiheujjO2VnOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhcm1TZXJ2ZXJSZXN0YXJ0RXhpdChyZXBseVJhdzogUmVwbHlSYXcpOiB2b2lkIHtcbiAgaWYgKCFyZXN0YXJ0UGVuZGluZyB8fCBleGl0QXJtZWQpIHJldHVybjtcbiAgZXhpdEFybWVkID0gdHJ1ZTtcblxuICBjb25zdCBleGl0T25jZSA9ICgpID0+IHtcbiAgICBpZiAoZXhpdFRyaWdnZXJlZCkgcmV0dXJuO1xuICAgIGV4aXRUcmlnZ2VyZWQgPSB0cnVlO1xuICAgIGNsZWFyRXhpdEFybSgpO1xuICAgIHByb2Nlc3MuZXhpdCgwKTtcbiAgfTtcblxuICBhcm1lZFJlcGx5UmF3ID0gcmVwbHlSYXc7XG4gIGFybWVkRXhpdCA9IGV4aXRPbmNlO1xuICByZXBseVJhdy5vbmNlKFwiZmluaXNoXCIsIGV4aXRPbmNlKTtcbiAgcmVwbHlSYXcub25jZShcImNsb3NlXCIsIGV4aXRPbmNlKTtcbiAgZmFsbGJhY2tUaW1lciA9IHNldFRpbWVvdXQoZXhpdE9uY2UsIFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyk7XG4gIGZhbGxiYWNrVGltZXIudW5yZWYoKTtcbiAgaWYgKHJlcGx5UmF3LmRlc3Ryb3llZCAmJiAhcmVwbHlSYXcud3JpdGFibGVFbmRlZCkgZXhpdE9uY2UoKTtcbn1cblxuLyoqIOuqqOuTiCDsoITsl60g7IOB7YOc66W8IOqyqeumrO2VtOyVvCDtlZjripQg64uo7JyEIO2FjOyKpO2KuCDsoITsmqkuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzZXRTZXJ2ZXJSZXN0YXJ0Rm9yVGVzdHMoKTogdm9pZCB7XG4gIGNsZWFyRXhpdEFybSgpO1xuICByZXN0YXJ0UGVuZGluZyA9IGZhbHNlO1xuICBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG4gIGV4aXRBcm1lZCA9IGZhbHNlO1xufVxuIl0sIm1hcHBpbmdzIjoiO0FBRUEsTUFBTSwyQkFBMkI7QUFFakMsSUFBSSxpQkFBaUI7QUFDckIsSUFBSSxnQkFBZ0I7QUFDcEIsSUFBSSxZQUFZO0FBQ2hCLElBQUk7QUFDSixJQUFJO0FBQ0osSUFBSTs7QUFLSixTQUFnQixtQkFBNEI7Q0FDMUMsT0FBTztBQUNUOzs7Ozs7QUFPQSxTQUFnQixxQkFBOEI7Q0FDNUMsSUFBSSxnQkFBZ0IsT0FBTztDQUMzQixpQkFBaUI7Q0FDakIsT0FBTztBQUNUO0FBRUEsU0FBUyxlQUFxQjtDQUM1QixJQUFJLGVBQWUsYUFBYSxhQUFhO0NBQzdDLGdCQUFnQjtDQUNoQixJQUFJLGlCQUFpQixXQUFXO0VBQzlCLGNBQWMsZUFBZSxVQUFVLFNBQVM7RUFDaEQsY0FBYyxlQUFlLFNBQVMsU0FBUztDQUNqRDtDQUNBLGdCQUFnQjtDQUNoQixZQUFZO0FBQ2Q7O0FBR0EsU0FBZ0IscUJBQXFCLFVBQTBCO0NBQzdELElBQUksQ0FBQyxrQkFBa0IsV0FBVztDQUNsQyxZQUFZO0NBRVosTUFBTSxpQkFBaUI7RUFDckIsSUFBSSxlQUFlO0VBQ25CLGdCQUFnQjtFQUNoQixhQUFhO0VBQ2IsUUFBUSxLQUFLLENBQUM7Q0FDaEI7Q0FFQSxnQkFBZ0I7Q0FDaEIsWUFBWTtDQUNaLFNBQVMsS0FBSyxVQUFVLFFBQVE7Q0FDaEMsU0FBUyxLQUFLLFNBQVMsUUFBUTtDQUMvQixnQkFBZ0IsV0FBVyxVQUFVLHdCQUF3QjtDQUM3RCxjQUFjLE1BQU07Q0FDcEIsSUFBSSxTQUFTLGFBQWEsQ0FBQyxTQUFTLGVBQWUsU0FBUztBQUM5RDs7QUFHQSxTQUFnQiw2QkFBbUM7Q0FDakQsYUFBYTtDQUNiLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsWUFBWTtBQUNkIn0=
@@ -52,6 +52,13 @@ vi.mock("../token/token.model", () => ({
52
52
  },
53
53
  }));
54
54
 
55
+ // OAuth 리다이렉트 테스트는 token-death를 검증하지 않는다. 부분 TokenModel mock이
56
+ // isolate:false 워커의 token-death 전용 테스트로 새지 않게 실제 모듈 로드를 막는다.
57
+ vi.mock("./token-death", () => ({
58
+ deactivateAuthDeadToken: vi.fn(),
59
+ notifyTokenAdded: vi.fn(),
60
+ }));
61
+
55
62
  function authUrlParam(authUrl: string, key: string): string | null {
56
63
  return new URL(authUrl).searchParams.get(key);
57
64
  }
@@ -1,10 +1,16 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
+ import {
4
+ beginServerRestart,
5
+ resetServerRestartForTests,
6
+ } from "../../utils/server-restart";
3
7
  import {
4
8
  afterQuery,
5
9
  beforeQuery,
10
+ finishActiveNativeRunsForRestart,
6
11
  finishRunAborted,
7
12
  finishRunWithError,
13
+ resetNativeRunRegistryForTests,
8
14
  } from "./qgrid-run-lifecycle";
9
15
  import { type QueryOutput } from "./qgrid.types";
10
16
 
@@ -58,6 +64,11 @@ function queryOutput(overrides: Partial<QueryOutput> = {}): QueryOutput {
58
64
 
59
65
  let lifecycleNow = Date.now();
60
66
 
67
+ afterEach(() => {
68
+ resetServerRestartForTests();
69
+ resetNativeRunRegistryForTests();
70
+ });
71
+
61
72
  describe("qgrid run lifecycle start", () => {
62
73
  beforeEach(() => {
63
74
  lifecycleNow += 60_001;
@@ -188,6 +199,108 @@ describe("qgrid run lifecycle start", () => {
188
199
  expect(expireStaleToolWaitingRunsMock).toHaveBeenCalledTimes(1);
189
200
  expect(createRunMock).toHaveBeenCalledTimes(2);
190
201
  });
202
+
203
+ it("rejects new native runs while a restart is pending", async () => {
204
+ beginServerRestart();
205
+
206
+ await expect(beforeQuery({ prompt: "hi" })).rejects.toMatchObject({ statusCode: 503 });
207
+ expect(createRunMock).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("finishes a run prepared during the restart race before rejecting provider execution", async () => {
211
+ let resolveCreate!: (requestLogId: number) => void;
212
+ createRunMock.mockReturnValueOnce(
213
+ new Promise<number>((resolve) => {
214
+ resolveCreate = resolve;
215
+ }),
216
+ );
217
+
218
+ const starting = beforeQuery({ prompt: "hi" });
219
+ await vi.waitFor(() => expect(createRunMock).toHaveBeenCalledOnce());
220
+ beginServerRestart();
221
+ resolveCreate(22);
222
+
223
+ await expect(starting).rejects.toMatchObject({ statusCode: 503 });
224
+ expect(finishRunMock).toHaveBeenCalledWith(
225
+ 22,
226
+ expect.objectContaining({ status: "error", error_message: "server restarted" }),
227
+ );
228
+ });
229
+
230
+ it("marks only active native runs as restarted and prevents a late success overwrite", async () => {
231
+ const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
232
+
233
+ await finishActiveNativeRunsForRestart();
234
+ expect(finishRunMock).toHaveBeenCalledWith(
235
+ requestLogId,
236
+ expect.objectContaining({ status: "error", error_message: "server restarted" }),
237
+ );
238
+
239
+ appendStepMock.mockClear();
240
+ finishRunMock.mockClear();
241
+ await expect(afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput())).resolves.toEqual(
242
+ {},
243
+ );
244
+ expect(appendStepMock).not.toHaveBeenCalled();
245
+ expect(finishRunMock).not.toHaveBeenCalled();
246
+ });
247
+
248
+ it("waits for an in-flight finalizer and writes the restart error last", async () => {
249
+ const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
250
+ let releaseAppend!: () => void;
251
+ appendStepMock.mockReturnValueOnce(
252
+ new Promise<void>((resolve) => {
253
+ releaseAppend = resolve;
254
+ }),
255
+ );
256
+
257
+ const finishing = afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput());
258
+ await vi.waitFor(() => expect(appendStepMock).toHaveBeenCalledOnce());
259
+ const restarting = finishActiveNativeRunsForRestart();
260
+
261
+ await Promise.resolve();
262
+ expect(finishRunMock).not.toHaveBeenCalledWith(
263
+ requestLogId,
264
+ expect.objectContaining({ error_message: "server restarted" }),
265
+ );
266
+
267
+ releaseAppend();
268
+ await Promise.all([finishing, restarting]);
269
+
270
+ const terminalStatuses = finishRunMock.mock.calls
271
+ .filter(([id]) => id === requestLogId)
272
+ .map(([, payload]) => payload.status);
273
+ expect(terminalStatuses.at(-1)).toBe("error");
274
+ expect(finishRunMock).toHaveBeenLastCalledWith(
275
+ requestLogId,
276
+ expect.objectContaining({ status: "error", error_message: "server restarted" }),
277
+ );
278
+ });
279
+
280
+ it("leaves tool-wait runs alone after their provider turn has completed", async () => {
281
+ const { requestLogId, stepIndex } = await beforeQuery({ prompt: "use a tool" });
282
+ await afterQuery(
283
+ requestLogId,
284
+ stepIndex,
285
+ { prompt: "use a tool" },
286
+ queryOutput({
287
+ finishReason: "tool-calls",
288
+ content: [
289
+ {
290
+ type: "tool-call",
291
+ toolCallId: "call-1",
292
+ toolName: "lookup",
293
+ input: "{}",
294
+ },
295
+ ],
296
+ }),
297
+ );
298
+ finishRunMock.mockClear();
299
+
300
+ await finishActiveNativeRunsForRestart();
301
+
302
+ expect(finishRunMock).not.toHaveBeenCalled();
303
+ });
191
304
  });
192
305
 
193
306
  describe("qgrid run lifecycle TTFT", () => {
@@ -1,5 +1,8 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
+ import { ServiceUnavailableException } from "sonamu";
2
3
 
4
+ import { SD } from "../../i18n/sd.generated";
5
+ import { isRestartPending } from "../../utils/server-restart";
3
6
  import { RequestLogModel } from "../request-log/request-log.model";
4
7
  import {
5
8
  estimateImageGenerationCostMicroUsd,
@@ -17,6 +20,43 @@ const STALE_RUN_THRESHOLD_MS = 30 * 60 * 1000;
17
20
  const STALE_CLEANUP_INTERVAL_MS = 60 * 1000;
18
21
  let staleCleanupInFlight: Promise<void> | undefined;
19
22
  let lastStaleCleanupStartedAt = 0;
23
+ const activeNativeRunRefs = new Map<number, number>();
24
+ const interruptedNativeRunIds = new Set<number>();
25
+ const activeNativeFinalizers = new Map<number, Set<Promise<void>>>();
26
+
27
+ function restartUnavailable(): ServiceUnavailableException {
28
+ return new ServiceUnavailableException(SD("qgrid.restartPending")());
29
+ }
30
+
31
+ function registerNativeRun(requestLogId: number): void {
32
+ activeNativeRunRefs.set(requestLogId, (activeNativeRunRefs.get(requestLogId) ?? 0) + 1);
33
+ }
34
+
35
+ function releaseNativeRun(requestLogId: number): void {
36
+ const refs = activeNativeRunRefs.get(requestLogId);
37
+ if (refs === undefined || refs <= 1) activeNativeRunRefs.delete(requestLogId);
38
+ else activeNativeRunRefs.set(requestLogId, refs - 1);
39
+ }
40
+
41
+ function trackNativeFinalizer<T>(requestLogId: number, finalize: () => Promise<T>): Promise<T> {
42
+ const operation = finalize();
43
+ const settled = operation.then(
44
+ () => undefined,
45
+ () => undefined,
46
+ );
47
+ const finalizers = activeNativeFinalizers.get(requestLogId) ?? new Set<Promise<void>>();
48
+ finalizers.add(settled);
49
+ activeNativeFinalizers.set(requestLogId, finalizers);
50
+ void settled.then(() => {
51
+ finalizers.delete(settled);
52
+ if (finalizers.size === 0) activeNativeFinalizers.delete(requestLogId);
53
+ });
54
+ return operation;
55
+ }
56
+
57
+ export function assertNativeRunAdmission(): void {
58
+ if (isRestartPending()) throw restartUnavailable();
59
+ }
20
60
 
21
61
  function withProviderPrefix(
22
62
  routeModel: string | undefined,
@@ -61,6 +101,8 @@ export async function beforeQuery(args: QueryInput): Promise<{
61
101
  requestLogId: number;
62
102
  stepIndex: number;
63
103
  }> {
104
+ assertNativeRunAdmission();
105
+
64
106
  let requestLogId: number;
65
107
  let stepIndex = 0;
66
108
 
@@ -88,6 +130,14 @@ export async function beforeQuery(args: QueryInput): Promise<{
88
130
  });
89
131
  }
90
132
 
133
+ // DB 준비 중에 재시작이 시작됐다면 provider 실행으로 넘어가지 않는다.
134
+ // 이 요청은 재시작 snapshot에 없었으므로 방금 준비한 run을 직접 마감한다.
135
+ if (isRestartPending()) {
136
+ await finishTerminalRun(requestLogId, "error", "server restarted");
137
+ throw restartUnavailable();
138
+ }
139
+
140
+ registerNativeRun(requestLogId);
91
141
  return { requestLogId, stepIndex };
92
142
  }
93
143
 
@@ -97,87 +147,97 @@ export async function afterQuery(
97
147
  args: QueryInput,
98
148
  result: QueryOutput,
99
149
  ): Promise<RunLifecycleResult> {
100
- // provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
101
- const requestedModelName = withProviderPrefix(
102
- args.model,
103
- args.model ?? result.requestedModel ?? result.model,
104
- );
105
- const servedModelName = withProviderPrefix(args.model, result.model);
106
-
107
- // generate step 기록
108
- await RequestLogModel.appendStep(requestLogId, {
109
- step_index: stepIndex,
110
- type: "generate",
111
- model_name: servedModelName,
112
- requested_model_name: requestedModelName,
113
- fallback_count: result.modelFallbacks?.length ?? 0,
114
- input_tokens: result.usage.input_tokens,
115
- output_tokens: result.usage.output_tokens,
116
- cache_read_tokens: result.usage.cache_read_input_tokens,
117
- cache_creation_tokens: result.usage.cache_creation_input_tokens,
118
- cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
119
- cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
120
- cost_usd: Math.round(result.costUsd * 1_000_000),
121
- cost_source: result.costSource,
122
- duration_ms: result.durationMs,
123
- ttft_ms: result.ttftMs,
124
- finish_reason: result.finishReason,
125
- });
150
+ if (interruptedNativeRunIds.has(requestLogId)) return {};
126
151
 
127
- const imageParts = getImageParts(result);
128
- const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
129
- result,
130
- args.imageGenerationOptions,
131
- );
132
- for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
133
- await RequestLogModel.appendStep(requestLogId, step);
134
- }
152
+ return trackNativeFinalizer(requestLogId, async () => {
153
+ try {
154
+ // provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
155
+ const requestedModelName = withProviderPrefix(
156
+ args.model,
157
+ args.model ?? result.requestedModel ?? result.model,
158
+ );
159
+ const servedModelName = withProviderPrefix(args.model, result.model);
135
160
 
136
- if (result.finishReason === "tool-calls") {
137
- // tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
138
- const toolCalls = result.content.filter((c) => c.type === "tool-call");
139
- for (let i = 0; i < toolCalls.length; i++) {
140
- const tc = toolCalls[i]!;
141
- if (tc.type !== "tool-call") continue;
161
+ // generate step 기록
142
162
  await RequestLogModel.appendStep(requestLogId, {
143
163
  step_index: stepIndex,
144
- type: "tool_call",
145
- tool_call_index: imageParts.length + i,
146
- tool_call_id: tc.toolCallId,
147
- tool_name: tc.toolName,
148
- tool_args: tc.input,
164
+ type: "generate",
165
+ model_name: servedModelName,
166
+ requested_model_name: requestedModelName,
167
+ fallback_count: result.modelFallbacks?.length ?? 0,
168
+ input_tokens: result.usage.input_tokens,
169
+ output_tokens: result.usage.output_tokens,
170
+ cache_read_tokens: result.usage.cache_read_input_tokens,
171
+ cache_creation_tokens: result.usage.cache_creation_input_tokens,
172
+ cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
173
+ cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
174
+ cost_usd: Math.round(result.costUsd * 1_000_000),
175
+ cost_source: result.costSource,
176
+ duration_ms: result.durationMs,
177
+ ttft_ms: result.ttftMs,
178
+ finish_reason: result.finishReason,
149
179
  });
150
- }
151
180
 
152
- return { runContext: { requestLogId } };
153
- }
181
+ const imageParts = getImageParts(result);
182
+ const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
183
+ result,
184
+ args.imageGenerationOptions,
185
+ );
186
+ for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
187
+ await RequestLogModel.appendStep(requestLogId, step);
188
+ }
154
189
 
155
- // finishReason === "stop" → run 종료 (전체 step usage 합산)
156
- const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
157
- await RequestLogModel.finishRun(requestLogId, {
158
- status: "succeeded",
159
- response: formatResponseForLog(result),
160
- token_name: result.tokenName,
161
- input_tokens: agg.input_tokens,
162
- output_tokens: agg.output_tokens,
163
- cache_read_tokens: agg.cache_read_tokens,
164
- cache_creation_tokens: agg.cache_creation_tokens,
165
- cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
166
- cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
167
- duration_ms: agg.duration_ms,
168
- // multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
169
- requested_model_name: requestedModelName,
170
- model_name: servedModelName,
171
- fallback_count: agg.fallback_count,
172
- cost_usd: agg.cost_usd,
173
- cost_source: agg.cost_source,
174
- image_cost_usd: imageCostMicroUsd,
175
- image_cost_method:
176
- imageCostMicroUsd !== null ? imageGenerationCostMethod(args.imageGenerationOptions) : null,
177
- history: filterHistoryForStorage(args.history),
178
- });
190
+ if (result.finishReason === "tool-calls") {
191
+ // tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
192
+ const toolCalls = result.content.filter((c) => c.type === "tool-call");
193
+ for (let i = 0; i < toolCalls.length; i++) {
194
+ const tc = toolCalls[i]!;
195
+ if (tc.type !== "tool-call") continue;
196
+ await RequestLogModel.appendStep(requestLogId, {
197
+ step_index: stepIndex,
198
+ type: "tool_call",
199
+ tool_call_index: imageParts.length + i,
200
+ tool_call_id: tc.toolCallId,
201
+ tool_name: tc.toolName,
202
+ tool_args: tc.input,
203
+ });
204
+ }
205
+
206
+ return { runContext: { requestLogId } };
207
+ }
208
+
209
+ // finishReason === "stop" → run 종료 (전체 step usage 합산)
210
+ const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
211
+ await RequestLogModel.finishRun(requestLogId, {
212
+ status: "succeeded",
213
+ response: formatResponseForLog(result),
214
+ token_name: result.tokenName,
215
+ input_tokens: agg.input_tokens,
216
+ output_tokens: agg.output_tokens,
217
+ cache_read_tokens: agg.cache_read_tokens,
218
+ cache_creation_tokens: agg.cache_creation_tokens,
219
+ cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
220
+ cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
221
+ duration_ms: agg.duration_ms,
222
+ // multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
223
+ requested_model_name: requestedModelName,
224
+ model_name: servedModelName,
225
+ fallback_count: agg.fallback_count,
226
+ cost_usd: agg.cost_usd,
227
+ cost_source: agg.cost_source,
228
+ image_cost_usd: imageCostMicroUsd,
229
+ image_cost_method:
230
+ imageCostMicroUsd !== null
231
+ ? imageGenerationCostMethod(args.imageGenerationOptions)
232
+ : null,
233
+ history: filterHistoryForStorage(args.history),
234
+ });
179
235
 
180
- return {};
236
+ return {};
237
+ } finally {
238
+ releaseNativeRun(requestLogId);
239
+ }
240
+ });
181
241
  }
182
242
 
183
243
  export async function finishRunWithError(
@@ -185,11 +245,48 @@ export async function finishRunWithError(
185
245
  errorMessage: string,
186
246
  args?: QueryInput,
187
247
  ): Promise<void> {
188
- await finishTerminalRun(requestLogId, "error", errorMessage, args);
248
+ if (interruptedNativeRunIds.has(requestLogId)) return;
249
+ return trackNativeFinalizer(requestLogId, async () => {
250
+ try {
251
+ await finishTerminalRun(requestLogId, "error", errorMessage, args);
252
+ } finally {
253
+ releaseNativeRun(requestLogId);
254
+ }
255
+ });
189
256
  }
190
257
 
191
258
  export async function finishRunAborted(requestLogId: number, args?: QueryInput): Promise<void> {
192
- await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
259
+ if (interruptedNativeRunIds.has(requestLogId)) return;
260
+ return trackNativeFinalizer(requestLogId, async () => {
261
+ try {
262
+ await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
263
+ } finally {
264
+ releaseNativeRun(requestLogId);
265
+ }
266
+ });
267
+ }
268
+
269
+ /** 현재 이 프로세스가 provider를 실행 중인 native run만 재시작 오류로 마감한다. */
270
+ export async function finishActiveNativeRunsForRestart(): Promise<void> {
271
+ const requestLogIds = [...activeNativeRunRefs.keys()];
272
+ for (const requestLogId of requestLogIds) {
273
+ interruptedNativeRunIds.add(requestLogId);
274
+ activeNativeRunRefs.delete(requestLogId);
275
+ }
276
+
277
+ await Promise.all(
278
+ requestLogIds.map(async (requestLogId) => {
279
+ await Promise.all(activeNativeFinalizers.get(requestLogId) ?? []);
280
+ await finishTerminalRun(requestLogId, "error", "server restarted");
281
+ }),
282
+ );
283
+ }
284
+
285
+ /** 프로세스 전역 registry를 격리해야 하는 단위 테스트 전용. */
286
+ export function resetNativeRunRegistryForTests(): void {
287
+ activeNativeRunRefs.clear();
288
+ interruptedNativeRunIds.clear();
289
+ activeNativeFinalizers.clear();
193
290
  }
194
291
 
195
292
  async function finishTerminalRun(