@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
1 lines • 8.91 kB
Source Map (JSON)
{"version":3,"file":"lifecycle-loading-tracker.cjs","names":["#store","#isDisposed","#onSettled","#ignore","#lastTerminalLifecycleSeq","#lastRunningLifecycleSeq","isRootNamespace"],"sources":["../../src/stream/lifecycle-loading-tracker.ts"],"sourcesContent":["/**\n * Drives the {@link RootSnapshot.isLoading} flag from root lifecycle\n * events.\n *\n * # What it does\n *\n * The tracker watches a stream of protocol events and flips the\n * `isLoading` slot of a {@link StreamStore} based on root-namespace\n * `lifecycle` payloads:\n *\n * - `running` → `isLoading = true`\n * - `completed` / `failed` / `interrupted`\n * → `isLoading = false`\n *\n * Non-root, non-lifecycle, and unknown events are ignored.\n *\n * # Why it lives in its own class\n *\n * Lifecycle handling has two subtleties that we want to keep out of\n * the {@link StreamController}'s critical path:\n *\n * 1. **Stale `running` filtering.** SSE replays older events on\n * reconnect — including a `running` lifecycle that fired before\n * the run terminated. Without filtering, that replay would flip\n * `isLoading` back to `true` after a `completed` already brought\n * it down. We track the highest terminal `seq` we've seen and\n * drop any `running` whose `seq` is at or below it.\n * 2. **Deferred terminal flip.** The flip from `true → false` is\n * pushed to the next macrotask (`setTimeout(..., 0)`). This\n * gives synchronous consumers — most notably `for await`\n * iterators in framework bindings — one event-loop tick to\n * observe terminal-related state (e.g. the final assistant\n * message landing in `values`) before `isLoading` settles.\n * The deferred callback re-checks whether a newer `running`\n * arrived in the meantime (HITL interrupt → resume, or SSE\n * history replay of interrupted → running) and bails if so,\n * so a stale terminal reset cannot stomp an active run.\n *\n * # Why it's safe to register the listener as `controller.onEvent`\n *\n * The tracker subscribes to the controller's root event bus via the\n * exported {@link listener} arrow. Because the listener is bound at\n * construction time, removing it later (`bus.delete(tracker.listener)`)\n * works without `bind()` gymnastics in the controller.\n *\n * @typeParam T - The snapshot shape; must contain an `isLoading` flag.\n */\nimport type { Event, LifecycleEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"./store.js\";\nimport { isRootNamespace } from \"./namespace.js\";\n\n/**\n * Minimal contract the snapshot must satisfy. The tracker only\n * touches `isLoading`, leaving everything else for the controller.\n */\ntype LoadingSnapshot = { readonly isLoading: boolean };\n\n/**\n * Drives root-snapshot `isLoading` from root lifecycle events.\n */\nexport class LifecycleLoadingTracker<T extends LoadingSnapshot> {\n /** Snapshot store whose `isLoading` slot we manage. */\n readonly #store: StreamStore<T>;\n\n /**\n * Disposal probe. Consulted from the deferred `setTimeout` so a\n * controller torn down between scheduling and firing doesn't end\n * up writing to a defunct store.\n */\n readonly #isDisposed: () => boolean;\n\n /**\n * Highest sequence number of a terminal lifecycle we've observed.\n * `running` events at or below this seq are stale replays and\n * are dropped to avoid flipping the loading flag back on after the\n * run has already ended.\n */\n #lastTerminalLifecycleSeq = -1;\n\n /**\n * Highest sequence number of a `running` lifecycle we've observed.\n * Deferred terminal resets compare against this so an\n * `interrupted`/`completed`/`failed` whose macrotask fires after a\n * newer `running` does not incorrectly clear `isLoading`.\n */\n #lastRunningLifecycleSeq = -1;\n\n /** Notified once `isLoading` actually settles to `false` on a terminal event. */\n readonly #onSettled: (() => void) | undefined;\n\n readonly #ignore: ((event: Event) => boolean) | undefined;\n\n /**\n * @param params.store - Store whose `isLoading` slot we drive.\n * @param params.isDisposed - Disposal probe consulted from deferred callbacks.\n * @param params.onSettled - Called once `isLoading` flips to `false`,\n * including for a run this client only observed and never\n * dispatched itself.\n * @param params.ignore - Lifecycle events to skip, e.g. another run's\n * replayed terminal while this client's own run is in flight.\n */\n constructor(params: {\n store: StreamStore<T>;\n isDisposed: () => boolean;\n onSettled?: () => void;\n ignore?: (event: Event) => boolean;\n }) {\n this.#store = params.store;\n this.#isDisposed = params.isDisposed;\n this.#onSettled = params.onSettled;\n this.#ignore = params.ignore;\n }\n\n /**\n * Bound listener suitable for `EventBus.subscribe`. Re-exposed as a\n * stable property so the controller can later remove the same\n * function reference from the bus on teardown.\n */\n readonly listener = (event: Event): void => {\n this.handle(event);\n };\n\n /**\n * Reset internal state when rebinding to a new thread.\n *\n * The seq guards are per-thread: a new thread's lifecycle events\n * are not stale relative to the old thread's.\n */\n reset(): void {\n this.#lastTerminalLifecycleSeq = -1;\n this.#lastRunningLifecycleSeq = -1;\n }\n\n /**\n * Process a single protocol event.\n *\n * Filters down to root-namespace lifecycle events, then mutates the\n * store's `isLoading` slot. All other events are ignored.\n *\n * @param event - Any protocol event from the controller's root bus.\n */\n handle(event: Event): void {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n if (this.#ignore?.(event)) return;\n const lifecycle = (event as LifecycleEvent).params.data as {\n event?: string;\n };\n const seq = typeof event.seq === \"number\" ? event.seq : undefined;\n if (lifecycle?.event === \"running\") {\n // Drop stale `running` replays that arrive *after* a terminal\n // for the same run. SSE re-streams history on reconnect; without\n // this filter the loading flag would oscillate.\n if (seq != null && seq <= this.#lastTerminalLifecycleSeq) {\n return;\n }\n if (seq != null) {\n this.#lastRunningLifecycleSeq = Math.max(\n this.#lastRunningLifecycleSeq,\n seq\n );\n }\n this.#store.setState((s) =>\n s.isLoading ? s : { ...s, isLoading: true }\n );\n return;\n }\n if (\n lifecycle?.event === \"completed\" ||\n lifecycle?.event === \"failed\" ||\n lifecycle?.event === \"interrupted\"\n ) {\n if (seq != null) {\n this.#lastTerminalLifecycleSeq = Math.max(\n this.#lastTerminalLifecycleSeq,\n seq\n );\n }\n // Flip `isLoading=false` on the next macrotask so synchronous\n // consumers iterating events get one tick to observe terminal\n // state (the final values snapshot etc.) before the loading\n // indicator drops. Bail if a newer `running` arrived since this\n // terminal was scheduled (answered HITL interrupt, back-to-back\n // runs, or history replay of interrupted → running).\n setTimeout(() => {\n if (this.#isDisposed()) return;\n if (seq != null && this.#lastRunningLifecycleSeq > seq) return;\n const wasLoading = this.#store.getSnapshot().isLoading;\n this.#store.setState((s) =>\n s.isLoading ? { ...s, isLoading: false } : s\n );\n if (wasLoading) this.#onSettled?.();\n }, 0);\n }\n }\n}\n"],"mappings":";;;;;AA4DA,IAAa,0BAAb,MAAgE;;CAE9D;;;;;;CAOA;;;;;;;CAQA,4BAA4B;;;;;;;CAQ5B,2BAA2B;;CAG3B;CAEA;;;;;;;;;;CAWA,YAAY,QAKT;AACD,QAAA,QAAc,OAAO;AACrB,QAAA,aAAmB,OAAO;AAC1B,QAAA,YAAkB,OAAO;AACzB,QAAA,SAAe,OAAO;;;;;;;CAQxB,YAAqB,UAAuB;AAC1C,OAAK,OAAO,MAAM;;;;;;;;CASpB,QAAc;AACZ,QAAA,2BAAiC;AACjC,QAAA,0BAAgC;;;;;;;;;;CAWlC,OAAO,OAAoB;AACzB,MAAI,MAAM,WAAW,YAAa;AAClC,MAAI,CAACM,kBAAAA,gBAAgB,MAAM,OAAO,UAAU,CAAE;AAC9C,MAAI,MAAA,SAAe,MAAM,CAAE;EAC3B,MAAM,YAAa,MAAyB,OAAO;EAGnD,MAAM,MAAM,OAAO,MAAM,QAAQ,WAAW,MAAM,MAAM,KAAA;AACxD,MAAI,WAAW,UAAU,WAAW;AAIlC,OAAI,OAAO,QAAQ,OAAO,MAAA,yBACxB;AAEF,OAAI,OAAO,KACT,OAAA,0BAAgC,KAAK,IACnC,MAAA,yBACA,IACD;AAEH,SAAA,MAAY,UAAU,MACpB,EAAE,YAAY,IAAI;IAAE,GAAG;IAAG,WAAW;IAAM,CAC5C;AACD;;AAEF,MACE,WAAW,UAAU,eACrB,WAAW,UAAU,YACrB,WAAW,UAAU,eACrB;AACA,OAAI,OAAO,KACT,OAAA,2BAAiC,KAAK,IACpC,MAAA,0BACA,IACD;AAQH,oBAAiB;AACf,QAAI,MAAA,YAAkB,CAAE;AACxB,QAAI,OAAO,QAAQ,MAAA,0BAAgC,IAAK;IACxD,MAAM,aAAa,MAAA,MAAY,aAAa,CAAC;AAC7C,UAAA,MAAY,UAAU,MACpB,EAAE,YAAY;KAAE,GAAG;KAAG,WAAW;KAAO,GAAG,EAC5C;AACD,QAAI,WAAY,OAAA,aAAmB;MAClC,EAAE"}