{"version":3,"file":"session-lock.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/session-lock.ts"],"sourcesContent":["import type { SessionLock } from \"../contracts/orchestrator/session-lock.contract\";\n\n/**\n * Reason an aborted lock-wait rejects with — the signal's `reason` when\n * one was supplied to `controller.abort(reason)`, else a generic error.\n */\nfunction abortReason(signal: AbortSignal): unknown {\n  return signal.reason ?? new Error(\"session lock wait aborted\");\n}\n\n/**\n * Framework-default in-process {@link SessionLock} — a per-key promise-\n * chain mutex. It holds one tail promise per session key; each acquirer\n * waits on the previous holder's release, then installs its own tail.\n * Keyed by `sessionId`, so different sessions never contend. The wait is\n * abortable, so a cancelled caller never deadlocks behind a stuck\n * predecessor (the deadlock-on-cancel trap).\n *\n * In-process only: serializes same-session turns within ONE process.\n * Supply a distributed {@link SessionLock} for multi-process deployments.\n */\nexport function inProcessSessionLock(): SessionLock {\n  const tails = new Map<string, Promise<void>>();\n\n  return {\n    async withLock<T>(\n      key: string,\n      fn: () => Promise<T>,\n      options?: { signal?: AbortSignal },\n    ): Promise<T> {\n      const existing = tails.get(key);\n      const prev = existing ?? Promise.resolve();\n\n      // The caller's signal aborts a genuine WAIT only. When the lock is\n      // free (no existing tail) we acquire immediately and let `fn` own\n      // cancellation — so a pre-aborted signal never pre-empts graceful\n      // in-flight handling (e.g. the orchestrator emitting turn.cancelled).\n      const waitSignal = existing ? options?.signal : undefined;\n\n      let release!: () => void;\n      const held = new Promise<void>(resolve => {\n        release = resolve;\n      });\n\n      // Successors queue behind OUR release. A predecessor that rejects\n      // still lets us through (both branches resolve to `held`), so a\n      // single failed turn never wedges the whole session.\n      const mine = prev.then(\n        () => held,\n        () => held,\n      );\n      tails.set(key, mine);\n\n      const cleanup = () => {\n        // Drop the map entry once we're the tail, so idle sessions don't\n        // leak Promise references.\n        if (tails.get(key) === mine) {\n          tails.delete(key);\n        }\n      };\n\n      try {\n        await waitForTurn(prev, waitSignal);\n      } catch (error) {\n        // Never acquired the critical section — release immediately so\n        // successors aren't blocked by an aborted waiter, then surface\n        // the abort to the caller.\n        release();\n        cleanup();\n        throw error;\n      }\n\n      try {\n        return await fn();\n      } finally {\n        release();\n        cleanup();\n      }\n    },\n  };\n}\n\n/**\n * Wait for `prev` (the previous holder's release) to settle, racing it\n * against `signal` so a cancelled caller stops waiting instead of\n * deadlocking. Predecessor rejections are swallowed — a failed turn still\n * releases the lock to the next waiter.\n */\nfunction waitForTurn(prev: Promise<void>, signal?: AbortSignal): Promise<void> {\n  const settled = prev.then(\n    () => {},\n    () => {},\n  );\n\n  if (!signal) return settled;\n  if (signal.aborted) return Promise.reject(abortReason(signal));\n\n  return new Promise<void>((resolve, reject) => {\n    const onAbort = () => reject(abortReason(signal));\n    signal.addEventListener(\"abort\", onAbort, { once: true });\n    void settled.then(() => {\n      signal.removeEventListener(\"abort\", onAbort);\n      resolve();\n    });\n  });\n}\n\n/**\n * No-op {@link SessionLock} for `sessionLock: false` — runs `fn` with no\n * serialization at all. Opt out only when an external mechanism (sticky\n * routing, a single-writer guarantee) already serializes same-session\n * turns.\n */\nexport function noopSessionLock(): SessionLock {\n  return {\n    withLock<T>(_key: string, fn: () => Promise<T>): Promise<T> {\n      return fn();\n    },\n  };\n}\n"],"mappings":";;;;;AAMA,SAAS,YAAY,QAA8B;CACjD,OAAO,OAAO,0BAAU,IAAI,MAAM,2BAA2B;AAC/D;;;;;;;;;;;;AAaA,SAAgB,uBAAoC;CAClD,MAAM,wBAAQ,IAAI,IAA2B;CAE7C,OAAO,EACL,MAAM,SACJ,KACA,IACA,SACY;EACZ,MAAM,WAAW,MAAM,IAAI,GAAG;EAC9B,MAAM,OAAO,YAAY,QAAQ,QAAQ;EAMzC,MAAM,aAAa,WAAW,SAAS,SAAS;EAEhD,IAAI;EACJ,MAAM,OAAO,IAAI,SAAc,YAAW;GACxC,UAAU;EACZ,CAAC;EAKD,MAAM,OAAO,KAAK,WACV,YACA,IACR;EACA,MAAM,IAAI,KAAK,IAAI;EAEnB,MAAM,gBAAgB;GAGpB,IAAI,MAAM,IAAI,GAAG,MAAM,MACrB,MAAM,OAAO,GAAG;EAEpB;EAEA,IAAI;GACF,MAAM,YAAY,MAAM,UAAU;EACpC,SAAS,OAAO;GAId,QAAQ;GACR,QAAQ;GACR,MAAM;EACR;EAEA,IAAI;GACF,OAAO,MAAM,GAAG;EAClB,UAAU;GACR,QAAQ;GACR,QAAQ;EACV;CACF,EACF;AACF;;;;;;;AAQA,SAAS,YAAY,MAAqB,QAAqC;CAC7E,MAAM,UAAU,KAAK,WACb,CAAC,SACD,CAAC,CACT;CAEA,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI,OAAO,SAAS,OAAO,QAAQ,OAAO,YAAY,MAAM,CAAC;CAE7D,OAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,gBAAgB,OAAO,YAAY,MAAM,CAAC;EAChD,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACxD,AAAK,QAAQ,WAAW;GACtB,OAAO,oBAAoB,SAAS,OAAO;GAC3C,QAAQ;EACV,CAAC;CACH,CAAC;AACH;;;;;;;AAQA,SAAgB,kBAA+B;CAC7C,OAAO,EACL,SAAY,MAAc,IAAkC;EAC1D,OAAO,GAAG;CACZ,EACF;AACF"}