detached.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /**
  2. * Quiescence tracking for emit-shaped hook runs that no extension point awaits. Bridges
  3. * track the run plus its continuation, pass the tracker signal into execution,
  4. * and drain on disposal so no process or late callback outlives the fiber.
  5. * @module @deepseek-ai/dsh-hook-protocol/detached
  6. */
  7. /** In-flight registry for one bridge's detached hook runs; see the module doc for the wiring contract. */
  8. export interface DetachedRuns {
  9. /**
  10. * The abort signal every tracked run must hand to {@link runHook} (via its
  11. * `signal` option). {@link drain} fires it so a still-running hook process is
  12. * killed rather than awaited out to its timeout (default 10 minutes).
  13. */
  14. readonly signal: AbortSignal
  15. /**
  16. * Register one detached run until it settles. Pass the FULL chain — the hook
  17. * run and its continuation/error handler — so {@link drain} waits for the
  18. * side effects (an inject, a warn), not just the process exit. A rejected
  19. * chain is absorbed here (settlement bookkeeping only), but rejection
  20. * handling is still the caller's job: an untracked `.catch` is what turns a
  21. * failure into a logged warning instead of silence.
  22. * @param run - the detached run chain to hold until settled.
  23. */
  24. track(run: Promise<unknown>): void
  25. /**
  26. * Abort {@link signal}, then resolve once every tracked chain has settled —
  27. * including chains tracked while the drain is in progress. The bridge
  28. * registers this as its effect disposer; cordis awaits it, so
  29. * `fiber.dispose()` resolving means the bridge's detached work is quiescent.
  30. * A run tracked AFTER drain resolves is not awaited by anyone — by then the
  31. * bridge's listeners are disposed, so nothing can start one.
  32. * @returns resolves when all tracked runs have settled.
  33. */
  34. drain(): Promise<void>
  35. }
  36. /**
  37. * Create a {@link DetachedRuns} tracker (one per bridge `apply()`); settled
  38. * runs are pruned so a long-lived session does not accumulate them.
  39. * @returns the tracker.
  40. */
  41. export function createDetachedRuns(): DetachedRuns {
  42. const inflight = new Set<Promise<unknown>>()
  43. const controller = new AbortController()
  44. return {
  45. signal: controller.signal,
  46. track(run: Promise<unknown>): void {
  47. inflight.add(run)
  48. const settled = (): void => { inflight.delete(run) }
  49. void run.then(settled, settled)
  50. },
  51. async drain(): Promise<void> {
  52. controller.abort(new Error('hook bridge disposed'))
  53. // Re-check after each wave: a chain can be tracked while a prior wave is
  54. // settling; loop until the registry is observed empty.
  55. while (inflight.size > 0) {
  56. await Promise.allSettled([...inflight])
  57. }
  58. },
  59. }
  60. }