input-wait-tracker.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. const IDLESS_INPUT_WAIT = Symbol('idless-input-wait');
  2. const INPUT_WAIT_ASK_EVENTS = {
  3. 'permission.asked': 'permission',
  4. 'question.asked': 'question',
  5. } as const;
  6. const INPUT_WAIT_RESOLUTION_EVENTS = {
  7. 'permission.replied': 'permission',
  8. 'question.replied': 'question',
  9. 'question.rejected': 'question',
  10. } as const;
  11. function isInputWaitAskEvent(
  12. type: string,
  13. ): type is keyof typeof INPUT_WAIT_ASK_EVENTS {
  14. return Object.hasOwn(INPUT_WAIT_ASK_EVENTS, type);
  15. }
  16. function isInputWaitResolutionEvent(
  17. type: string,
  18. ): type is keyof typeof INPUT_WAIT_RESOLUTION_EVENTS {
  19. return Object.hasOwn(INPUT_WAIT_RESOLUTION_EVENTS, type);
  20. }
  21. function inputWaitKey(kind: 'permission' | 'question', requestID: string) {
  22. return `${kind}:${requestID}`;
  23. }
  24. export function createInputWaitTracker(options: {
  25. shouldManageSession: (sessionID: string) => boolean;
  26. invalidateContinuation: (sessionID: string) => void;
  27. }) {
  28. const inputWaitsByParent = new Map<string, Set<string | symbol>>();
  29. function hasInputWait(sessionID: string): boolean {
  30. return (inputWaitsByParent.get(sessionID)?.size ?? 0) > 0;
  31. }
  32. function clearInputWaits(sessionID: string): void {
  33. inputWaitsByParent.delete(sessionID);
  34. }
  35. function trackInputWait(event: {
  36. type: string;
  37. properties?: { id?: string; requestID?: string; sessionID?: string };
  38. }): void {
  39. const sessionID = event.properties?.sessionID;
  40. if (!sessionID || !options.shouldManageSession(sessionID)) {
  41. return;
  42. }
  43. if (isInputWaitAskEvent(event.type)) {
  44. const requestID = event.properties?.id;
  45. const waits =
  46. inputWaitsByParent.get(sessionID) ?? new Set<string | symbol>();
  47. if (!requestID) {
  48. waits.add(IDLESS_INPUT_WAIT);
  49. inputWaitsByParent.set(sessionID, waits);
  50. options.invalidateContinuation(sessionID);
  51. return;
  52. }
  53. const key = inputWaitKey(INPUT_WAIT_ASK_EVENTS[event.type], requestID);
  54. waits.add(key);
  55. inputWaitsByParent.set(sessionID, waits);
  56. options.invalidateContinuation(sessionID);
  57. return;
  58. }
  59. if (!isInputWaitResolutionEvent(event.type)) return;
  60. const requestID = event.properties?.requestID;
  61. if (!requestID) return;
  62. const key = inputWaitKey(
  63. INPUT_WAIT_RESOLUTION_EVENTS[event.type],
  64. requestID,
  65. );
  66. const waits = inputWaitsByParent.get(sessionID);
  67. if (!waits) return;
  68. waits.delete(key);
  69. if (waits.size === 0) clearInputWaits(sessionID);
  70. }
  71. return {
  72. trackInputWait,
  73. hasInputWait,
  74. clearInputWaits,
  75. // Exposed for consumers not yet migrated (disposed handler, etc.)
  76. waitsByParent: inputWaitsByParent,
  77. };
  78. }