UNPKG

@langchain/langgraph

Version:
1 lines 8.91 kB
{"version":3,"file":"subscription.cjs","names":[],"sources":["../../src/stream/subscription.ts"],"sourcesContent":["/**\n * Subscription matching and channel inference for the v2 streaming protocol.\n *\n * These helpers are the building blocks a custom transport / server needs to\n * fan protocol events out to subscribers: map an event to its logical\n * {@link Channel} ({@link inferChannel}) and decide whether a buffered event\n * should be delivered for a given {@link SubscribeParams} filter\n * ({@link matchesSubscription}). They are typed against the minimal\n * {@link MatchableEvent} shape so the same predicate works on the core\n * {@link ProtocolEvent} produced by {@link convertToProtocolEvent} /\n * {@link StreamChannel} and on the wire-level `Event` from\n * `@langchain/protocol`.\n */\n\nimport type { Channel, SubscribeParams } from \"@langchain/protocol\";\nimport type { Namespace } from \"./types.js\";\n\n/**\n * Minimal protocol-event shape consumed by {@link inferChannel} and\n * {@link matchesSubscription}.\n *\n * Both the core {@link ProtocolEvent} and the wire-level `Event` from\n * `@langchain/protocol` structurally satisfy this contract, so the same\n * predicates can drive in-process fan-out, buffered replay, and server-side\n * (SSE / WebSocket) event-sink filtering without coupling to a single event\n * type.\n */\nexport interface MatchableEvent {\n /** Logical stream channel; see {@link inferChannel}. */\n readonly method: string;\n\n /** Monotonic sequence number, when present. Used by the `since` cursor. */\n readonly seq?: number;\n\n readonly params: {\n /** Namespace of the node or scope that emitted this event. */\n readonly namespace: Namespace;\n\n /** Opaque channel payload; shape depends on `method`. */\n readonly data?: unknown;\n };\n}\n\n/**\n * Strip dynamic suffixes (after `:`) from a namespace segment.\n *\n * Server-emitted namespaces contain runtime-generated suffixes like\n * `\"fetcher:abc-uuid\"`, while user-supplied subscription filters are typically\n * static names (`\"fetcher\"`). Mirrors `normalize_namespace_segment` in\n * `api/langgraph_api/protocol/namespace.py`.\n *\n * @param segment - Raw namespace segment.\n * @returns The stable graph-oriented portion of the segment.\n */\nexport function normalizeNamespaceSegment(segment: string): string {\n const idx = segment.indexOf(\":\");\n return idx === -1 ? segment : segment.slice(0, idx);\n}\n\n/**\n * Whether `namespace` starts with `prefix`.\n *\n * Segments are compared literally first; if the prefix segment itself contains\n * no `:`, the candidate segment is also compared after its dynamic suffix is\n * stripped (see {@link normalizeNamespaceSegment}). This mirrors\n * `is_prefix_match` in `api/langgraph_api/protocol/namespace.py` so server-side\n * filtering and client-side per-subscription narrowing stay consistent.\n *\n * @param namespace - Event namespace to test.\n * @param prefix - Subscription namespace prefix.\n */\nexport function isPrefixMatch(\n namespace: Namespace,\n prefix: Namespace\n): boolean {\n if (prefix.length > namespace.length) return false;\n for (let i = 0; i < prefix.length; i += 1) {\n const segment = prefix[i]!;\n const candidate = namespace[i]!;\n if (candidate === segment) continue;\n if (segment.includes(\":\")) return false;\n if (normalizeNamespaceSegment(candidate) === segment) continue;\n return false;\n }\n return true;\n}\n\nfunction namespaceMatches(\n eventNamespace: Namespace,\n prefixes: Namespace[] | undefined,\n depth: number | undefined\n): boolean {\n if (!prefixes || prefixes.length === 0) {\n return true;\n }\n\n return prefixes.some((prefix) => {\n if (!isPrefixMatch(eventNamespace, prefix)) return false;\n if (depth === undefined) return true;\n return eventNamespace.length - prefix.length <= depth;\n });\n}\n\n/**\n * The base protocol subscription channels, excluding the templated\n * `custom:<name>` form. This is the runtime counterpart to the `Channel`\n * union from `@langchain/protocol` and mirrors the channel set a server\n * recognizes when filtering its event sinks.\n */\nexport const SUPPORTED_CHANNELS = new Set<Channel>([\n \"values\",\n \"updates\",\n \"messages\",\n \"tools\",\n \"lifecycle\",\n \"input\",\n \"checkpoints\",\n \"tasks\",\n \"custom\",\n]);\n\n/**\n * Whether `value` names a protocol subscription channel — either a base\n * channel (`\"messages\"`, `\"values\"`, …) or a named custom channel\n * (`\"custom:<name>\"`). Unknown/future method names return `false`,\n * mirroring {@link inferChannel}.\n *\n * @param value - Candidate channel name.\n */\nexport function isSupportedChannel(value: string): value is Channel {\n return (\n SUPPORTED_CHANNELS.has(value as Channel) || value.startsWith(\"custom:\")\n );\n}\n\n/**\n * Maps a protocol event method to its subscription {@link Channel}.\n *\n * Returns `undefined` for unrecognized methods so that new server-side\n * channels (e.g. from extension transformers) don't break existing\n * subscribers. The `custom` method resolves to the named `custom:<name>`\n * channel when the payload carries a `name`, otherwise the bare `custom`\n * channel. Both `\"input\"` and the wire-level `\"input.requested\"` map to the\n * `input` channel.\n *\n * @param event - Event whose method should be mapped to a channel.\n */\nexport function inferChannel(event: MatchableEvent): Channel | undefined {\n switch (event.method) {\n case \"values\":\n return \"values\";\n case \"checkpoints\":\n return \"checkpoints\";\n case \"updates\":\n return \"updates\";\n case \"messages\":\n return \"messages\";\n case \"tools\":\n return \"tools\";\n case \"custom\": {\n const data = event.params.data as { name?: string } | undefined;\n return data?.name != null ? `custom:${data.name}` : \"custom\";\n }\n case \"lifecycle\":\n return \"lifecycle\";\n case \"input\":\n case \"input.requested\":\n return \"input\";\n case \"tasks\":\n return \"tasks\";\n default:\n return undefined;\n }\n}\n\n/**\n * Returns whether an event should be delivered for a subscription definition.\n *\n * When the definition carries a `since` replay cursor, events at or before\n * that sequence number are excluded — letting the same predicate drive both\n * live fan-out and buffered replay over a {@link StreamChannel}.\n *\n * @param event - Event being checked for delivery.\n * @param definition - Subscription filter definition to evaluate against.\n * The optional `since` field (a `seq` cursor) is read leniently because it\n * is not a declared field on the base {@link SubscribeParams} shape.\n */\nexport function matchesSubscription(\n event: MatchableEvent,\n definition: SubscribeParams\n): boolean {\n const since = (definition as { since?: unknown }).since;\n if (typeof since === \"number\" && (event.seq ?? 0) <= since) {\n return false;\n }\n\n const channel = inferChannel(event);\n if (channel === undefined) return false;\n\n const channels = definition.channels;\n const channelMatched =\n channels.includes(channel) ||\n (channel.startsWith(\"custom:\") && channels.includes(\"custom\"));\n if (!channelMatched) {\n return false;\n }\n\n return namespaceMatches(\n event.params.namespace,\n definition.namespaces,\n definition.depth\n );\n}\n"],"mappings":";;;;;;;;;;;;AAsDA,SAAgB,0BAA0B,SAAyB;CACjE,MAAM,MAAM,QAAQ,QAAQ,IAAI;AAChC,QAAO,QAAQ,KAAK,UAAU,QAAQ,MAAM,GAAG,IAAI;;;;;;;;;;;;;;AAerD,SAAgB,cACd,WACA,QACS;AACT,KAAI,OAAO,SAAS,UAAU,OAAQ,QAAO;AAC7C,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;EACzC,MAAM,UAAU,OAAO;EACvB,MAAM,YAAY,UAAU;AAC5B,MAAI,cAAc,QAAS;AAC3B,MAAI,QAAQ,SAAS,IAAI,CAAE,QAAO;AAClC,MAAI,0BAA0B,UAAU,KAAK,QAAS;AACtD,SAAO;;AAET,QAAO;;AAGT,SAAS,iBACP,gBACA,UACA,OACS;AACT,KAAI,CAAC,YAAY,SAAS,WAAW,EACnC,QAAO;AAGT,QAAO,SAAS,MAAM,WAAW;AAC/B,MAAI,CAAC,cAAc,gBAAgB,OAAO,CAAE,QAAO;AACnD,MAAI,UAAU,KAAA,EAAW,QAAO;AAChC,SAAO,eAAe,SAAS,OAAO,UAAU;GAChD;;;;;;;;AASJ,MAAa,qBAAqB,IAAI,IAAa;CACjD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;AAUF,SAAgB,mBAAmB,OAAiC;AAClE,QACE,mBAAmB,IAAI,MAAiB,IAAI,MAAM,WAAW,UAAU;;;;;;;;;;;;;;AAgB3E,SAAgB,aAAa,OAA4C;AACvE,SAAQ,MAAM,QAAd;EACE,KAAK,SACH,QAAO;EACT,KAAK,cACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,KAAK,QACH,QAAO;EACT,KAAK,UAAU;GACb,MAAM,OAAO,MAAM,OAAO;AAC1B,UAAO,MAAM,QAAQ,OAAO,UAAU,KAAK,SAAS;;EAEtD,KAAK,YACH,QAAO;EACT,KAAK;EACL,KAAK,kBACH,QAAO;EACT,KAAK,QACH,QAAO;EACT,QACE;;;;;;;;;;;;;;;AAgBN,SAAgB,oBACd,OACA,YACS;CACT,MAAM,QAAS,WAAmC;AAClD,KAAI,OAAO,UAAU,aAAa,MAAM,OAAO,MAAM,MACnD,QAAO;CAGT,MAAM,UAAU,aAAa,MAAM;AACnC,KAAI,YAAY,KAAA,EAAW,QAAO;CAElC,MAAM,WAAW,WAAW;AAI5B,KAAI,EAFF,SAAS,SAAS,QAAQ,IACzB,QAAQ,WAAW,UAAU,IAAI,SAAS,SAAS,SAAS,EAE7D,QAAO;AAGT,QAAO,iBACL,MAAM,OAAO,WACb,WAAW,YACX,WAAW,MACZ"}