{"version":3,"file":"HookRegistry.mjs","sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type { HookEvent, HookMatcher } from './types';\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n  private readonly global: MatcherBucket = {};\n  private readonly sessions: Map<string, MatcherBucket> = new Map();\n\n  /**\n   * Register a matcher for the lifetime of this registry (= one Run).\n   * Returns an unregister function that removes the matcher by reference.\n   */\n  register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n    const list = ensureList(this.global, event);\n    list.push(widen(matcher));\n    return () => {\n      removeFromList(list, matcher);\n    };\n  }\n\n  /**\n   * Register a matcher for a specific session. Cleared automatically when\n   * `clearSession(sessionId)` is called, or can be removed directly via the\n   * returned unregister function.\n   */\n  registerSession<E extends HookEvent>(\n    sessionId: string,\n    event: E,\n    matcher: HookMatcher<E>\n  ): () => void {\n    const bucket = this.ensureSessionBucket(sessionId);\n    const list = ensureList(bucket, event);\n    list.push(widen(matcher));\n    return () => {\n      removeFromList(list, matcher);\n    };\n  }\n\n  /**\n   * Returns all matchers registered for `event`, concatenating global first\n   * and then session-specific (when `sessionId` is supplied). The caller\n   * receives a fresh array, so iterating it is safe even if a matcher is\n   * removed mid-iteration (e.g. via `once: true`).\n   */\n  getMatchers<E extends HookEvent>(\n    event: E,\n    sessionId?: string\n  ): HookMatcher<E>[] {\n    const globalList = readList(this.global, event);\n    if (sessionId === undefined) {\n      return snapshot<E>(globalList);\n    }\n    const bucket = this.sessions.get(sessionId);\n    if (bucket === undefined) {\n      return snapshot<E>(globalList);\n    }\n    const sessionList = readList(bucket, event);\n    if (globalList.length === 0) {\n      return snapshot<E>(sessionList);\n    }\n    if (sessionList.length === 0) {\n      return snapshot<E>(globalList);\n    }\n    return snapshot<E>([...globalList, ...sessionList]);\n  }\n\n  /**\n   * Removes `matcher` by reference from global storage first, falling back\n   * to the session bucket when `sessionId` is supplied. Used by\n   * `executeHooks` to drop `once: true` matchers after they fire.\n   */\n  removeMatcher<E extends HookEvent>(\n    event: E,\n    matcher: HookMatcher<E>,\n    sessionId?: string\n  ): boolean {\n    if (removeFromList(readList(this.global, event), matcher)) {\n      return true;\n    }\n    if (sessionId === undefined) {\n      return false;\n    }\n    const bucket = this.sessions.get(sessionId);\n    if (bucket === undefined) {\n      return false;\n    }\n    return removeFromList(readList(bucket, event), matcher);\n  }\n\n  /**\n   * Drops every session-scoped matcher for `sessionId`. Call this in the\n   * `finally` block around a Run so a `once: true` hook that never fired\n   * cannot leak into the next session on the same registry.\n   */\n  clearSession(sessionId: string): void {\n    this.sessions.delete(sessionId);\n  }\n\n  /** True if at least one matcher exists for `event` (global + session). */\n  hasHookFor(event: HookEvent, sessionId?: string): boolean {\n    if (readList(this.global, event).length > 0) {\n      return true;\n    }\n    if (sessionId === undefined) {\n      return false;\n    }\n    const bucket = this.sessions.get(sessionId);\n    if (bucket === undefined) {\n      return false;\n    }\n    return readList(bucket, event).length > 0;\n  }\n\n  private ensureSessionBucket(sessionId: string): MatcherBucket {\n    const existing = this.sessions.get(sessionId);\n    if (existing !== undefined) {\n      return existing;\n    }\n    const fresh: MatcherBucket = {};\n    this.sessions.set(sessionId, fresh);\n    return fresh;\n  }\n}\n\nfunction ensureList(\n  bucket: MatcherBucket,\n  event: HookEvent\n): HookMatcher<HookEvent>[] {\n  const existing = bucket[event];\n  if (existing !== undefined) {\n    return existing;\n  }\n  const fresh: HookMatcher<HookEvent>[] = [];\n  bucket[event] = fresh;\n  return fresh;\n}\n\nfunction readList(\n  bucket: MatcherBucket,\n  event: HookEvent\n): HookMatcher<HookEvent>[] {\n  return bucket[event] ?? [];\n}\n\nfunction removeFromList<E extends HookEvent>(\n  list: HookMatcher<HookEvent>[],\n  matcher: HookMatcher<E>\n): boolean {\n  const idx = list.indexOf(widen(matcher));\n  if (idx < 0) {\n    return false;\n  }\n  list.splice(idx, 1);\n  return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n  matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n  return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n  list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n  return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"names":[],"mappings":"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"}