@langchain/langgraph-sdk 1.9.21 → 1.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/base.cjs +4 -1
- package/dist/client/base.cjs.map +1 -1
- package/dist/client/base.d.cts +2 -0
- package/dist/client/base.d.cts.map +1 -1
- package/dist/client/base.d.ts +2 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +5 -2
- package/dist/client/base.js.map +1 -1
- package/dist/client/runs/index.cjs +2 -0
- package/dist/client/runs/index.cjs.map +1 -1
- package/dist/client/runs/index.d.cts +15 -0
- package/dist/client/runs/index.d.cts.map +1 -1
- package/dist/client/runs/index.d.ts +15 -0
- package/dist/client/runs/index.d.ts.map +1 -1
- package/dist/client/runs/index.js +2 -0
- package/dist/client/runs/index.js.map +1 -1
- package/dist/client/stream/transport/agent-server.cjs +6 -1
- package/dist/client/stream/transport/agent-server.cjs.map +1 -1
- package/dist/client/stream/transport/agent-server.d.cts +10 -2
- package/dist/client/stream/transport/agent-server.d.cts.map +1 -1
- package/dist/client/stream/transport/agent-server.d.ts +10 -2
- package/dist/client/stream/transport/agent-server.d.ts.map +1 -1
- package/dist/client/stream/transport/agent-server.js +6 -1
- package/dist/client/stream/transport/agent-server.js.map +1 -1
- package/dist/client/stream/transport/http.cjs +33 -12
- package/dist/client/stream/transport/http.cjs.map +1 -1
- package/dist/client/stream/transport/http.d.cts +18 -7
- package/dist/client/stream/transport/http.d.cts.map +1 -1
- package/dist/client/stream/transport/http.d.ts +18 -7
- package/dist/client/stream/transport/http.d.ts.map +1 -1
- package/dist/client/stream/transport/http.js +35 -14
- package/dist/client/stream/transport/http.js.map +1 -1
- package/dist/client/stream/transport/types.d.cts +41 -5
- package/dist/client/stream/transport/types.d.cts.map +1 -1
- package/dist/client/stream/transport/types.d.ts +41 -5
- package/dist/client/stream/transport/types.d.ts.map +1 -1
- package/dist/client/stream/transport/utils.cjs +19 -0
- package/dist/client/stream/transport/utils.cjs.map +1 -1
- package/dist/client/stream/transport/utils.js +19 -1
- package/dist/client/stream/transport/utils.js.map +1 -1
- package/dist/client/stream/transport/websocket.cjs +20 -5
- package/dist/client/stream/transport/websocket.cjs.map +1 -1
- package/dist/client/stream/transport/websocket.d.cts +13 -4
- package/dist/client/stream/transport/websocket.d.cts.map +1 -1
- package/dist/client/stream/transport/websocket.d.ts +13 -4
- package/dist/client/stream/transport/websocket.d.ts.map +1 -1
- package/dist/client/stream/transport/websocket.js +21 -6
- package/dist/client/stream/transport/websocket.js.map +1 -1
- package/dist/client/stream/transport.d.cts +20 -3
- package/dist/client/stream/transport.d.cts.map +1 -1
- package/dist/client/stream/transport.d.ts +20 -3
- package/dist/client/stream/transport.d.ts.map +1 -1
- package/dist/client/stream/types.d.cts +14 -0
- package/dist/client/stream/types.d.cts.map +1 -1
- package/dist/client/stream/types.d.ts +14 -0
- package/dist/client/stream/types.d.ts.map +1 -1
- package/dist/client/threads/index.cjs +7 -4
- package/dist/client/threads/index.cjs.map +1 -1
- package/dist/client/threads/index.js +6 -3
- package/dist/client/threads/index.js.map +1 -1
- package/dist/react-ui/server/server.cjs +2 -2
- package/dist/react-ui/server/server.cjs.map +1 -1
- package/dist/react-ui/server/server.js +1 -1
- package/dist/react-ui/server/server.js.map +1 -1
- package/dist/stream/channel-registry.cjs +20 -7
- package/dist/stream/channel-registry.cjs.map +1 -1
- package/dist/stream/channel-registry.d.cts +4 -3
- package/dist/stream/channel-registry.d.cts.map +1 -1
- package/dist/stream/channel-registry.d.ts +4 -3
- package/dist/stream/channel-registry.d.ts.map +1 -1
- package/dist/stream/channel-registry.js +20 -7
- package/dist/stream/channel-registry.js.map +1 -1
- package/dist/stream/controller.cjs +2 -2
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.js +1 -1
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/submit-coordinator.cjs +3 -3
- package/dist/stream/submit-coordinator.cjs.map +1 -1
- package/dist/stream/submit-coordinator.js +1 -1
- package/dist/stream/submit-coordinator.js.map +1 -1
- package/dist/types.d.cts +20 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/index.d.cts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/stream.cjs +94 -0
- package/dist/utils/stream.cjs.map +1 -1
- package/dist/utils/stream.d.cts +15 -1
- package/dist/utils/stream.d.cts.map +1 -1
- package/dist/utils/stream.d.ts +15 -1
- package/dist/utils/stream.d.ts.map +1 -1
- package/dist/utils/stream.js +94 -1
- package/dist/utils/stream.js.map +1 -1
- package/package.json +3 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["uuidv7"],"sources":["../../../src/client/threads/index.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"uuid\";\n\nimport {\n Checkpoint,\n Config,\n DefaultValues,\n Metadata,\n SortOrder,\n Thread,\n ThreadSelectField,\n ThreadSortBy,\n ThreadState,\n ThreadStatus,\n ThreadValuesFilter,\n} from \"../../schema.js\";\nimport type { Command, OnConflictBehavior, StreamEvent } from \"../../types.js\";\nimport type { ThreadStreamMode } from \"../../types.stream.js\";\nimport { BaseClient } from \"../base.js\";\nimport { ThreadStream } from \"../stream/index.js\";\nimport type {\n ThreadStreamOptions,\n ThreadStreamTransportKind,\n} from \"../stream/types.js\";\nimport { ProtocolSseTransportAdapter } from \"../stream/transport/http.js\";\nimport { ProtocolWebSocketTransportAdapter } from \"../stream/transport/websocket.js\";\nimport type { TransportAdapter } from \"../stream/transport.js\";\n\nexport class ThreadsClient<\n TStateType = DefaultValues,\n TUpdateType = TStateType,\n> extends BaseClient {\n /**\n * Get a thread by ID.\n *\n * @param threadId ID of the thread.\n * @returns The thread.\n */\n async get<ValuesType = TStateType>(\n threadId: string,\n options?: { signal?: AbortSignal; include?: string[] }\n ): Promise<Thread<ValuesType>> {\n return this.fetch<Thread<ValuesType>>(`/threads/${threadId}`, {\n params: {\n include: options?.include ?? undefined,\n },\n signal: options?.signal,\n });\n }\n\n /**\n * Create a new thread.\n *\n * @param payload Payload for creating a thread.\n * @returns The created thread.\n */\n async create(payload?: {\n metadata?: Metadata;\n threadId?: string;\n ifExists?: OnConflictBehavior;\n graphId?: string;\n supersteps?: Array<{\n updates: Array<{ values: unknown; command?: Command; asNode: string }>;\n }>;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n signal?: AbortSignal;\n }): Promise<Thread<TStateType>> {\n const ttlPayload =\n typeof payload?.ttl === \"number\"\n ? { ttl: payload.ttl, strategy: \"delete\" as const }\n : payload?.ttl;\n\n return this.fetch<Thread<TStateType>>(`/threads`, {\n method: \"POST\",\n json: {\n metadata: {\n ...payload?.metadata,\n graph_id: payload?.graphId,\n },\n thread_id: payload?.threadId,\n if_exists: payload?.ifExists,\n supersteps: payload?.supersteps?.map((s) => ({\n updates: s.updates.map((u) => ({\n values: u.values,\n command: u.command,\n as_node: u.asNode,\n })),\n })),\n ttl: ttlPayload,\n },\n signal: payload?.signal,\n });\n }\n\n /**\n * Copy an existing thread\n * @param threadId ID of the thread to be copied\n * @returns Newly copied thread\n */\n async copy(\n threadId: string,\n options?: { signal?: AbortSignal }\n ): Promise<Thread<TStateType>> {\n return this.fetch<Thread<TStateType>>(`/threads/${threadId}/copy`, {\n method: \"POST\",\n signal: options?.signal,\n });\n }\n\n /**\n * Update a thread.\n *\n * @param threadId ID of the thread.\n * @param payload Payload for updating the thread.\n * @returns The updated thread.\n */\n async update(\n threadId: string,\n payload?: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal?: false;\n signal?: AbortSignal;\n }\n ): Promise<Thread>;\n async update(\n threadId: string,\n payload: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal: true;\n signal?: AbortSignal;\n }\n ): Promise<void>;\n async update(\n threadId: string,\n payload: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal: boolean;\n signal?: AbortSignal;\n }\n ): Promise<Thread | void>;\n async update(\n threadId: string,\n payload?: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal?: boolean;\n signal?: AbortSignal;\n }\n ): Promise<Thread | void> {\n const ttlPayload =\n typeof payload?.ttl === \"number\"\n ? { ttl: payload.ttl, strategy: \"delete\" as const }\n : payload?.ttl;\n\n return this.fetch<Thread | void>(`/threads/${threadId}`, {\n method: \"PATCH\",\n headers: payload?.returnMinimal\n ? { Prefer: \"return=minimal\" }\n : undefined,\n json: { metadata: payload?.metadata, ttl: ttlPayload },\n signal: payload?.signal,\n });\n }\n\n /**\n * Delete a thread.\n *\n * @param threadId ID of the thread.\n */\n async delete(\n threadId: string,\n options?: { signal?: AbortSignal }\n ): Promise<void> {\n return this.fetch<void>(`/threads/${threadId}`, {\n method: \"DELETE\",\n signal: options?.signal,\n });\n }\n\n /**\n * Prune threads by ID. The 'delete' strategy removes threads entirely.\n * The 'keep_latest' strategy prunes old checkpoints but keeps threads\n * and their latest state.\n *\n * @param threadIds List of thread IDs to prune.\n * @param options Additional options for pruning.\n * @param options.strategy The prune strategy. Defaults to 'delete'.\n * @param options.signal Signal to abort the request.\n * @returns An object containing `pruned_count`.\n */\n async prune(\n threadIds: string[],\n options?: {\n strategy?: \"delete\" | \"keep_latest\";\n signal?: AbortSignal;\n }\n ): Promise<{ pruned_count: number }> {\n return this.fetch<{ pruned_count: number }>(\"/threads/prune\", {\n method: \"POST\",\n json: {\n thread_ids: threadIds,\n strategy: options?.strategy ?? \"delete\",\n },\n signal: options?.signal,\n });\n }\n\n /**\n * List threads\n *\n * @param query Query options\n * @returns List of threads\n */\n async search<ValuesType = TStateType>(query?: {\n metadata?: Metadata;\n ids?: string[];\n limit?: number;\n offset?: number;\n status?: ThreadStatus;\n sortBy?: ThreadSortBy;\n sortOrder?: SortOrder;\n select?: ThreadSelectField[];\n values?: ThreadValuesFilter;\n extract?: Record<string, string>;\n signal?: AbortSignal;\n }): Promise<Thread<ValuesType>[]> {\n return this.fetch<Thread<ValuesType>[]>(\"/threads/search\", {\n method: \"POST\",\n json: {\n metadata: query?.metadata ?? undefined,\n ids: query?.ids ?? undefined,\n limit: query?.limit ?? 10,\n offset: query?.offset ?? 0,\n status: query?.status,\n sort_by: query?.sortBy,\n sort_order: query?.sortOrder,\n select: query?.select ?? undefined,\n values: query?.values ?? undefined,\n extract: query?.extract ?? undefined,\n },\n signal: query?.signal,\n });\n }\n\n /**\n * Count threads matching filters.\n *\n * @param query.metadata Thread metadata to filter on.\n * @param query.values State values to filter on.\n * @param query.status Thread status to filter on.\n * @returns Number of threads matching the criteria.\n */\n async count<ValuesType = TStateType>(query?: {\n metadata?: Metadata;\n values?: ValuesType;\n status?: ThreadStatus;\n signal?: AbortSignal;\n }): Promise<number> {\n return this.fetch<number>(`/threads/count`, {\n method: \"POST\",\n json: {\n metadata: query?.metadata ?? undefined,\n values: query?.values ?? undefined,\n status: query?.status ?? undefined,\n },\n signal: query?.signal,\n });\n }\n\n /**\n * Get state for a thread.\n *\n * @param threadId ID of the thread.\n * @returns Thread state.\n */\n async getState<ValuesType = TStateType>(\n threadId: string,\n checkpoint?: Checkpoint | string,\n options?: { subgraphs?: boolean; signal?: AbortSignal }\n ): Promise<ThreadState<ValuesType>> {\n if (checkpoint != null) {\n if (typeof checkpoint !== \"string\") {\n return this.fetch<ThreadState<ValuesType>>(\n `/threads/${threadId}/state/checkpoint`,\n {\n method: \"POST\",\n json: { checkpoint, subgraphs: options?.subgraphs },\n signal: options?.signal,\n }\n );\n }\n\n // deprecated\n return this.fetch<ThreadState<ValuesType>>(\n `/threads/${threadId}/state/${checkpoint}`,\n { params: { subgraphs: options?.subgraphs }, signal: options?.signal }\n );\n }\n\n return this.fetch<ThreadState<ValuesType>>(`/threads/${threadId}/state`, {\n params: { subgraphs: options?.subgraphs },\n signal: options?.signal,\n // Coalesce concurrent identical reads (e.g. two controllers\n // hydrating the same thread on reconnect). Skipped automatically\n // when a caller supplies its own `signal`.\n dedupe: true,\n });\n }\n\n /**\n * Add state to a thread.\n *\n * @param threadId The ID of the thread.\n * @returns\n */\n async updateState<ValuesType = TUpdateType>(\n threadId: string,\n options: {\n values: ValuesType;\n checkpoint?: Checkpoint;\n checkpointId?: string;\n asNode?: string;\n signal?: AbortSignal;\n }\n ): Promise<Pick<Config, \"configurable\">> {\n return this.fetch<Pick<Config, \"configurable\">>(\n `/threads/${threadId}/state`,\n {\n method: \"POST\",\n json: {\n values: options.values,\n checkpoint: options.checkpoint,\n checkpoint_id: options.checkpointId,\n as_node: options?.asNode,\n },\n signal: options?.signal,\n }\n );\n }\n\n /**\n * Patch the metadata of a thread.\n *\n * @param threadIdOrConfig Thread ID or config to patch the state of.\n * @param metadata Metadata to patch the state with.\n */\n async patchState(\n threadIdOrConfig: string | Config,\n metadata: Metadata,\n options?: { signal?: AbortSignal }\n ): Promise<void> {\n let threadId: string;\n\n if (typeof threadIdOrConfig !== \"string\") {\n if (typeof threadIdOrConfig.configurable?.thread_id !== \"string\") {\n throw new Error(\n \"Thread ID is required when updating state with a config.\"\n );\n }\n threadId = threadIdOrConfig.configurable.thread_id;\n } else {\n threadId = threadIdOrConfig;\n }\n\n return this.fetch<void>(`/threads/${threadId}/state`, {\n method: \"PATCH\",\n json: { metadata },\n signal: options?.signal,\n });\n }\n\n /**\n * Get all past states for a thread.\n *\n * @param threadId ID of the thread.\n * @param options Additional options.\n * @returns List of thread states.\n */\n async getHistory<ValuesType = TStateType>(\n threadId: string,\n options?: {\n limit?: number;\n before?: Config;\n checkpoint?: Partial<Omit<Checkpoint, \"thread_id\">>;\n metadata?: Metadata;\n signal?: AbortSignal;\n }\n ): Promise<ThreadState<ValuesType>[]> {\n return this.fetch<ThreadState<ValuesType>[]>(\n `/threads/${threadId}/history`,\n {\n method: \"POST\",\n json: {\n limit: options?.limit ?? 10,\n before: options?.before,\n metadata: options?.metadata,\n checkpoint: options?.checkpoint,\n },\n signal: options?.signal,\n // `getHistory` is a read despite using POST — coalesce the\n // hydrate-time discovery seed when two controllers reconnect to\n // the same thread concurrently. Skipped when `signal` is set.\n dedupe: true,\n }\n );\n }\n\n async *joinStream(\n threadId: string,\n options?: {\n lastEventId?: string;\n streamMode?: ThreadStreamMode | ThreadStreamMode[];\n signal?: AbortSignal;\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ): AsyncGenerator<{ id?: string; event: StreamEvent; data: any }> {\n yield* this.streamWithRetry({\n endpoint: `/threads/${threadId}/stream`,\n method: \"GET\",\n signal: options?.signal,\n headers: options?.lastEventId\n ? { \"Last-Event-ID\": options.lastEventId }\n : undefined,\n params: options?.streamMode\n ? { stream_mode: options.streamMode }\n : undefined,\n });\n }\n\n /**\n * Open a protocol stream over the thread-centric v2 protocol.\n *\n * Returns a {@link ThreadStream} with lazy getters\n * (`.messages`, `.values`, `.toolCalls`, `.subgraphs`, `.subagents`,\n * `.output`) and `thread.run.start({ input, ... })` for starting runs.\n * Mirrors the in-process `graph.streamEvents(..., { version: \"v3\" })` API.\n *\n * The thread is bound to `options.assistantId` for its lifetime.\n * The wire transport defaults to SSE; pass `transport: \"websocket\"`\n * in options (or configure `streamProtocol: \"v2-websocket\"` on the\n * client) to use a WebSocket instead.\n *\n * @example New thread (UUID generated client-side)\n * ```ts\n * const thread = client.threads.stream({ assistantId: \"my-agent\" });\n * ```\n *\n * @example Attach to an existing thread\n * ```ts\n * const thread = client.threads.stream(threadId, { assistantId: \"my-agent\" });\n * ```\n *\n * @example WebSocket transport\n * ```ts\n * const thread = client.threads.stream({\n * assistantId: \"my-agent\",\n * transport: \"websocket\",\n * });\n * ```\n */\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n options: ThreadStreamOptions\n ): ThreadStream<TExtensions>;\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n threadId: string,\n options: ThreadStreamOptions\n ): ThreadStream<TExtensions>;\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n threadIdOrOptions: string | ThreadStreamOptions,\n maybeOptions?: ThreadStreamOptions\n ): ThreadStream<TExtensions> {\n const { threadId, options } =\n typeof threadIdOrOptions === \"string\"\n ? {\n threadId: threadIdOrOptions,\n options: maybeOptions as ThreadStreamOptions,\n }\n : { threadId: uuidv7(), options: threadIdOrOptions };\n\n // `transport` accepts either a preset string (`\"sse\"` / `\"websocket\"`)\n // or a custom {@link AgentServerAdapter}. A custom adapter replaces\n // the built-in factories entirely — this is the seam that lets users\n // point `useStream` at any agent server (including the thin wrappers\n // produced by `HttpAgentServerAdapter`).\n // When callers supply `fetch`, use it verbatim (tests, auth shims). Otherwise\n // route protocol HTTP and media URL fetches through AsyncCaller like REST.\n const userFetch = options.fetch;\n const protocolFetch =\n userFetch ?? this.asyncCaller.fetch.bind(this.asyncCaller);\n\n let transport: TransportAdapter;\n if (options.transport != null && typeof options.transport !== \"string\") {\n transport = options.transport;\n } else {\n const transportKind: ThreadStreamTransportKind =\n options.transport ??\n (this.streamProtocol === \"v2-websocket\" ? \"websocket\" : \"sse\");\n const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;\n\n /**\n * Common options for both transports.\n */\n const commonOpts = {\n apiUrl: this.apiUrl,\n threadId,\n defaultHeaders: this.defaultHeaders,\n onRequest: this.onRequest,\n maxReconnectAttempts,\n reconnectDelayMs: options.reconnectDelayMs,\n onReconnect: options.onReconnect,\n };\n\n transport =\n transportKind === \"websocket\"\n ? new ProtocolWebSocketTransportAdapter({\n ...commonOpts,\n webSocketFactory: options.webSocketFactory,\n })\n : new ProtocolSseTransportAdapter({\n ...commonOpts,\n fetch: userFetch,\n asyncCaller: userFetch ? undefined : this.asyncCaller,\n });\n }\n\n return new ThreadStream<TExtensions>(transport, {\n ...options,\n fetch: protocolFetch,\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["uuidv7"],"sources":["../../../src/client/threads/index.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"@langchain/core/utils/uuid\";\n\nimport {\n Checkpoint,\n Config,\n DefaultValues,\n Metadata,\n SortOrder,\n Thread,\n ThreadSelectField,\n ThreadSortBy,\n ThreadState,\n ThreadStatus,\n ThreadValuesFilter,\n} from \"../../schema.js\";\nimport type { Command, OnConflictBehavior, StreamEvent } from \"../../types.js\";\nimport type { ThreadStreamMode } from \"../../types.stream.js\";\nimport { BaseClient } from \"../base.js\";\nimport { ThreadStream } from \"../stream/index.js\";\nimport type {\n ThreadStreamOptions,\n ThreadStreamTransportKind,\n} from \"../stream/types.js\";\nimport { ProtocolSseTransportAdapter } from \"../stream/transport/http.js\";\nimport { ProtocolWebSocketTransportAdapter } from \"../stream/transport/websocket.js\";\nimport type { TransportAdapter } from \"../stream/transport.js\";\n\nexport class ThreadsClient<\n TStateType = DefaultValues,\n TUpdateType = TStateType,\n> extends BaseClient {\n /**\n * Get a thread by ID.\n *\n * @param threadId ID of the thread.\n * @returns The thread.\n */\n async get<ValuesType = TStateType>(\n threadId: string,\n options?: { signal?: AbortSignal; include?: string[] }\n ): Promise<Thread<ValuesType>> {\n return this.fetch<Thread<ValuesType>>(`/threads/${threadId}`, {\n params: {\n include: options?.include ?? undefined,\n },\n signal: options?.signal,\n });\n }\n\n /**\n * Create a new thread.\n *\n * @param payload Payload for creating a thread.\n * @returns The created thread.\n */\n async create(payload?: {\n metadata?: Metadata;\n threadId?: string;\n ifExists?: OnConflictBehavior;\n graphId?: string;\n supersteps?: Array<{\n updates: Array<{ values: unknown; command?: Command; asNode: string }>;\n }>;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n signal?: AbortSignal;\n }): Promise<Thread<TStateType>> {\n const ttlPayload =\n typeof payload?.ttl === \"number\"\n ? { ttl: payload.ttl, strategy: \"delete\" as const }\n : payload?.ttl;\n\n return this.fetch<Thread<TStateType>>(`/threads`, {\n method: \"POST\",\n json: {\n metadata: {\n ...payload?.metadata,\n graph_id: payload?.graphId,\n },\n thread_id: payload?.threadId,\n if_exists: payload?.ifExists,\n supersteps: payload?.supersteps?.map((s) => ({\n updates: s.updates.map((u) => ({\n values: u.values,\n command: u.command,\n as_node: u.asNode,\n })),\n })),\n ttl: ttlPayload,\n },\n signal: payload?.signal,\n });\n }\n\n /**\n * Copy an existing thread\n * @param threadId ID of the thread to be copied\n * @returns Newly copied thread\n */\n async copy(\n threadId: string,\n options?: { signal?: AbortSignal }\n ): Promise<Thread<TStateType>> {\n return this.fetch<Thread<TStateType>>(`/threads/${threadId}/copy`, {\n method: \"POST\",\n signal: options?.signal,\n });\n }\n\n /**\n * Update a thread.\n *\n * @param threadId ID of the thread.\n * @param payload Payload for updating the thread.\n * @returns The updated thread.\n */\n async update(\n threadId: string,\n payload?: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal?: false;\n signal?: AbortSignal;\n }\n ): Promise<Thread>;\n async update(\n threadId: string,\n payload: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal: true;\n signal?: AbortSignal;\n }\n ): Promise<void>;\n async update(\n threadId: string,\n payload: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal: boolean;\n signal?: AbortSignal;\n }\n ): Promise<Thread | void>;\n async update(\n threadId: string,\n payload?: {\n metadata?: Metadata;\n ttl?: number | { ttl: number; strategy?: \"delete\" };\n returnMinimal?: boolean;\n signal?: AbortSignal;\n }\n ): Promise<Thread | void> {\n const ttlPayload =\n typeof payload?.ttl === \"number\"\n ? { ttl: payload.ttl, strategy: \"delete\" as const }\n : payload?.ttl;\n\n return this.fetch<Thread | void>(`/threads/${threadId}`, {\n method: \"PATCH\",\n headers: payload?.returnMinimal\n ? { Prefer: \"return=minimal\" }\n : undefined,\n json: { metadata: payload?.metadata, ttl: ttlPayload },\n signal: payload?.signal,\n });\n }\n\n /**\n * Delete a thread.\n *\n * @param threadId ID of the thread.\n */\n async delete(\n threadId: string,\n options?: { signal?: AbortSignal }\n ): Promise<void> {\n return this.fetch<void>(`/threads/${threadId}`, {\n method: \"DELETE\",\n signal: options?.signal,\n });\n }\n\n /**\n * Prune threads by ID. The 'delete' strategy removes threads entirely.\n * The 'keep_latest' strategy prunes old checkpoints but keeps threads\n * and their latest state.\n *\n * @param threadIds List of thread IDs to prune.\n * @param options Additional options for pruning.\n * @param options.strategy The prune strategy. Defaults to 'delete'.\n * @param options.signal Signal to abort the request.\n * @returns An object containing `pruned_count`.\n */\n async prune(\n threadIds: string[],\n options?: {\n strategy?: \"delete\" | \"keep_latest\";\n signal?: AbortSignal;\n }\n ): Promise<{ pruned_count: number }> {\n return this.fetch<{ pruned_count: number }>(\"/threads/prune\", {\n method: \"POST\",\n json: {\n thread_ids: threadIds,\n strategy: options?.strategy ?? \"delete\",\n },\n signal: options?.signal,\n });\n }\n\n /**\n * List threads\n *\n * @param query Query options\n * @returns List of threads\n */\n async search<ValuesType = TStateType>(query?: {\n metadata?: Metadata;\n ids?: string[];\n limit?: number;\n offset?: number;\n status?: ThreadStatus;\n sortBy?: ThreadSortBy;\n sortOrder?: SortOrder;\n select?: ThreadSelectField[];\n values?: ThreadValuesFilter;\n extract?: Record<string, string>;\n signal?: AbortSignal;\n }): Promise<Thread<ValuesType>[]> {\n return this.fetch<Thread<ValuesType>[]>(\"/threads/search\", {\n method: \"POST\",\n json: {\n metadata: query?.metadata ?? undefined,\n ids: query?.ids ?? undefined,\n limit: query?.limit ?? 10,\n offset: query?.offset ?? 0,\n status: query?.status,\n sort_by: query?.sortBy,\n sort_order: query?.sortOrder,\n select: query?.select ?? undefined,\n values: query?.values ?? undefined,\n extract: query?.extract ?? undefined,\n },\n signal: query?.signal,\n });\n }\n\n /**\n * Count threads matching filters.\n *\n * @param query.metadata Thread metadata to filter on.\n * @param query.values State values to filter on.\n * @param query.status Thread status to filter on.\n * @returns Number of threads matching the criteria.\n */\n async count<ValuesType = TStateType>(query?: {\n metadata?: Metadata;\n values?: ValuesType;\n status?: ThreadStatus;\n signal?: AbortSignal;\n }): Promise<number> {\n return this.fetch<number>(`/threads/count`, {\n method: \"POST\",\n json: {\n metadata: query?.metadata ?? undefined,\n values: query?.values ?? undefined,\n status: query?.status ?? undefined,\n },\n signal: query?.signal,\n });\n }\n\n /**\n * Get state for a thread.\n *\n * @param threadId ID of the thread.\n * @returns Thread state.\n */\n async getState<ValuesType = TStateType>(\n threadId: string,\n checkpoint?: Checkpoint | string,\n options?: { subgraphs?: boolean; signal?: AbortSignal }\n ): Promise<ThreadState<ValuesType>> {\n if (checkpoint != null) {\n if (typeof checkpoint !== \"string\") {\n return this.fetch<ThreadState<ValuesType>>(\n `/threads/${threadId}/state/checkpoint`,\n {\n method: \"POST\",\n json: { checkpoint, subgraphs: options?.subgraphs },\n signal: options?.signal,\n }\n );\n }\n\n // deprecated\n return this.fetch<ThreadState<ValuesType>>(\n `/threads/${threadId}/state/${checkpoint}`,\n { params: { subgraphs: options?.subgraphs }, signal: options?.signal }\n );\n }\n\n return this.fetch<ThreadState<ValuesType>>(`/threads/${threadId}/state`, {\n params: { subgraphs: options?.subgraphs },\n signal: options?.signal,\n // Coalesce concurrent identical reads (e.g. two controllers\n // hydrating the same thread on reconnect). Skipped automatically\n // when a caller supplies its own `signal`.\n dedupe: true,\n });\n }\n\n /**\n * Add state to a thread.\n *\n * @param threadId The ID of the thread.\n * @returns\n */\n async updateState<ValuesType = TUpdateType>(\n threadId: string,\n options: {\n values: ValuesType;\n checkpoint?: Checkpoint;\n checkpointId?: string;\n asNode?: string;\n signal?: AbortSignal;\n }\n ): Promise<Pick<Config, \"configurable\">> {\n return this.fetch<Pick<Config, \"configurable\">>(\n `/threads/${threadId}/state`,\n {\n method: \"POST\",\n json: {\n values: options.values,\n checkpoint: options.checkpoint,\n checkpoint_id: options.checkpointId,\n as_node: options?.asNode,\n },\n signal: options?.signal,\n }\n );\n }\n\n /**\n * Patch the metadata of a thread.\n *\n * @param threadIdOrConfig Thread ID or config to patch the state of.\n * @param metadata Metadata to patch the state with.\n */\n async patchState(\n threadIdOrConfig: string | Config,\n metadata: Metadata,\n options?: { signal?: AbortSignal }\n ): Promise<void> {\n let threadId: string;\n\n if (typeof threadIdOrConfig !== \"string\") {\n if (typeof threadIdOrConfig.configurable?.thread_id !== \"string\") {\n throw new Error(\n \"Thread ID is required when updating state with a config.\"\n );\n }\n threadId = threadIdOrConfig.configurable.thread_id;\n } else {\n threadId = threadIdOrConfig;\n }\n\n return this.fetch<void>(`/threads/${threadId}/state`, {\n method: \"PATCH\",\n json: { metadata },\n signal: options?.signal,\n });\n }\n\n /**\n * Get all past states for a thread.\n *\n * @param threadId ID of the thread.\n * @param options Additional options.\n * @returns List of thread states.\n */\n async getHistory<ValuesType = TStateType>(\n threadId: string,\n options?: {\n limit?: number;\n before?: Config;\n checkpoint?: Partial<Omit<Checkpoint, \"thread_id\">>;\n metadata?: Metadata;\n signal?: AbortSignal;\n }\n ): Promise<ThreadState<ValuesType>[]> {\n return this.fetch<ThreadState<ValuesType>[]>(\n `/threads/${threadId}/history`,\n {\n method: \"POST\",\n json: {\n limit: options?.limit ?? 10,\n before: options?.before,\n metadata: options?.metadata,\n checkpoint: options?.checkpoint,\n },\n signal: options?.signal,\n // `getHistory` is a read despite using POST — coalesce the\n // hydrate-time discovery seed when two controllers reconnect to\n // the same thread concurrently. Skipped when `signal` is set.\n dedupe: true,\n }\n );\n }\n\n async *joinStream(\n threadId: string,\n options?: {\n lastEventId?: string;\n streamMode?: ThreadStreamMode | ThreadStreamMode[];\n signal?: AbortSignal;\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ): AsyncGenerator<{ id?: string; event: StreamEvent; data: any }> {\n yield* this.streamWithRetry({\n endpoint: `/threads/${threadId}/stream`,\n method: \"GET\",\n signal: options?.signal,\n headers: options?.lastEventId\n ? { \"Last-Event-ID\": options.lastEventId }\n : undefined,\n params: options?.streamMode\n ? { stream_mode: options.streamMode }\n : undefined,\n });\n }\n\n /**\n * Open a protocol stream over the thread-centric v2 protocol.\n *\n * Returns a {@link ThreadStream} with lazy getters\n * (`.messages`, `.values`, `.toolCalls`, `.subgraphs`, `.subagents`,\n * `.output`) and `thread.run.start({ input, ... })` for starting runs.\n * Mirrors the in-process `graph.streamEvents(..., { version: \"v3\" })` API.\n *\n * The thread is bound to `options.assistantId` for its lifetime.\n * The wire transport defaults to SSE; pass `transport: \"websocket\"`\n * in options (or configure `streamProtocol: \"v2-websocket\"` on the\n * client) to use a WebSocket instead.\n *\n * @example New thread (UUID generated client-side)\n * ```ts\n * const thread = client.threads.stream({ assistantId: \"my-agent\" });\n * ```\n *\n * @example Attach to an existing thread\n * ```ts\n * const thread = client.threads.stream(threadId, { assistantId: \"my-agent\" });\n * ```\n *\n * @example WebSocket transport\n * ```ts\n * const thread = client.threads.stream({\n * assistantId: \"my-agent\",\n * transport: \"websocket\",\n * });\n * ```\n */\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n options: ThreadStreamOptions\n ): ThreadStream<TExtensions>;\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n threadId: string,\n options: ThreadStreamOptions\n ): ThreadStream<TExtensions>;\n stream<TExtensions extends Record<string, unknown> = Record<string, unknown>>(\n threadIdOrOptions: string | ThreadStreamOptions,\n maybeOptions?: ThreadStreamOptions\n ): ThreadStream<TExtensions> {\n const { threadId, options } =\n typeof threadIdOrOptions === \"string\"\n ? {\n threadId: threadIdOrOptions,\n options: maybeOptions as ThreadStreamOptions,\n }\n : { threadId: uuidv7(), options: threadIdOrOptions };\n\n // `transport` accepts either a preset string (`\"sse\"` / `\"websocket\"`)\n // or a custom {@link AgentServerAdapter}. A custom adapter replaces\n // the built-in factories entirely — this is the seam that lets users\n // point `useStream` at any agent server (including the thin wrappers\n // produced by `HttpAgentServerAdapter`).\n // When callers supply `fetch`, use it verbatim (tests, auth shims). Otherwise\n // route protocol HTTP and media URL fetches through AsyncCaller like REST.\n const userFetch = options.fetch;\n const protocolFetch =\n userFetch ?? this.asyncCaller.fetch.bind(this.asyncCaller);\n\n let transport: TransportAdapter;\n if (options.transport != null && typeof options.transport !== \"string\") {\n transport = options.transport;\n // Bind (or re-bind) the caller-supplied adapter to this thread so a\n // single instance can follow lazy creation (the first `submit()` on a\n // `threadId: null` controller mints the id here) and thread switches.\n // No-op for adapters that bake `threadId` at construction.\n transport.setThreadId?.(threadId);\n } else {\n const transportKind: ThreadStreamTransportKind =\n options.transport ??\n (this.streamProtocol === \"v2-websocket\" ? \"websocket\" : \"sse\");\n const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;\n\n /**\n * Common options for both transports.\n */\n const commonOpts = {\n apiUrl: this.apiUrl,\n threadId,\n defaultHeaders: this.defaultHeaders,\n onRequest: this.onRequest,\n maxReconnectAttempts,\n reconnectDelayMs: options.reconnectDelayMs,\n onReconnect: options.onReconnect,\n };\n\n transport =\n transportKind === \"websocket\"\n ? new ProtocolWebSocketTransportAdapter({\n ...commonOpts,\n webSocketFactory: options.webSocketFactory,\n })\n : new ProtocolSseTransportAdapter({\n ...commonOpts,\n idleReconnect: options.streamIdleReconnect,\n fetch: userFetch,\n asyncCaller: userFetch ? undefined : this.asyncCaller,\n });\n }\n\n return new ThreadStream<TExtensions>(transport, {\n ...options,\n fetch: protocolFetch,\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require("../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
-
let
|
|
2
|
+
let _langchain_core_utils_uuid = require("@langchain/core/utils/uuid");
|
|
3
3
|
//#region src/react-ui/server/server.ts
|
|
4
4
|
/**
|
|
5
5
|
* Helper to send and persist UI messages. Accepts a map of component names to React components
|
|
@@ -17,7 +17,7 @@ const typedUi = (config, options) => {
|
|
|
17
17
|
function handlePush(message, options) {
|
|
18
18
|
const evt = {
|
|
19
19
|
type: "ui",
|
|
20
|
-
id: message?.id ?? (0,
|
|
20
|
+
id: message?.id ?? (0, _langchain_core_utils_uuid.v4)(),
|
|
21
21
|
name: message?.name,
|
|
22
22
|
props: message?.props,
|
|
23
23
|
metadata: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":[],"sources":["../../../src/react-ui/server/server.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"uuid\";\nimport type { ComponentPropsWithoutRef, ElementType } from \"react\";\nimport type { RemoveUIMessage, UIMessage } from \"../types.js\";\n\ninterface MessageLike {\n id?: string;\n}\n\n/**\n * Helper to send and persist UI messages. Accepts a map of component names to React components\n * as type argument to provide type safety. Will also write to the `options?.stateKey` state.\n *\n * @param config LangGraphRunnableConfig\n * @param options\n * @returns\n */\nexport const typedUi = <Decl extends Record<string, ElementType>>(\n config: {\n writer?: (chunk: unknown) => void;\n runId?: string;\n metadata?: Record<string, unknown>;\n tags?: string[];\n runName?: string;\n configurable?: {\n __pregel_send?: (writes_: [string, unknown][]) => void;\n [key: string]: unknown;\n };\n },\n options?: {\n /** The key to write the UI messages to. Defaults to `ui`. */\n stateKey?: string;\n }\n) => {\n type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };\n const items: (UIMessage | RemoveUIMessage)[] = [];\n const stateKey = options?.stateKey ?? \"ui\";\n\n const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;\n if (!runId) throw new Error(\"run_id is required\");\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K];\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K]>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options: { message?: MessageLike; merge: true }\n ): UIMessage<K, Partial<PropMap[K]>>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K] | Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {\n const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {\n type: \"ui\" as const,\n id: message?.id ?? uuidv4(),\n name: message?.name,\n props: message?.props,\n metadata: {\n merge: options?.merge || undefined,\n run_id: runId,\n tags: config.tags,\n name: config.runName,\n ...message?.metadata,\n ...(options?.message ? { id: options.message.id } : null),\n },\n };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n }\n\n const handleDelete = (id: string): RemoveUIMessage => {\n const evt: RemoveUIMessage = { type: \"remove-ui\", id };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n };\n\n return { push: handlePush, delete: handleDelete, items };\n};\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,WACX,QAWA,YAIG;CAEH,MAAM,QAAyC,EAAE;CACjD,MAAM,WAAW,SAAS,YAAY;CAEtC,MAAM,QAAS,OAAO,UAAU,UAAiC,OAAO;AACxE,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,qBAAqB;CAsBjD,SAAS,WACP,SAMA,SACgD;EAChD,MAAM,MAAsD;GAC1D,MAAM;GACN,IAAI,SAAS,OAAA,GAAA,
|
|
1
|
+
{"version":3,"file":"server.cjs","names":[],"sources":["../../../src/react-ui/server/server.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"@langchain/core/utils/uuid\";\nimport type { ComponentPropsWithoutRef, ElementType } from \"react\";\nimport type { RemoveUIMessage, UIMessage } from \"../types.js\";\n\ninterface MessageLike {\n id?: string;\n}\n\n/**\n * Helper to send and persist UI messages. Accepts a map of component names to React components\n * as type argument to provide type safety. Will also write to the `options?.stateKey` state.\n *\n * @param config LangGraphRunnableConfig\n * @param options\n * @returns\n */\nexport const typedUi = <Decl extends Record<string, ElementType>>(\n config: {\n writer?: (chunk: unknown) => void;\n runId?: string;\n metadata?: Record<string, unknown>;\n tags?: string[];\n runName?: string;\n configurable?: {\n __pregel_send?: (writes_: [string, unknown][]) => void;\n [key: string]: unknown;\n };\n },\n options?: {\n /** The key to write the UI messages to. Defaults to `ui`. */\n stateKey?: string;\n }\n) => {\n type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };\n const items: (UIMessage | RemoveUIMessage)[] = [];\n const stateKey = options?.stateKey ?? \"ui\";\n\n const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;\n if (!runId) throw new Error(\"run_id is required\");\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K];\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K]>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options: { message?: MessageLike; merge: true }\n ): UIMessage<K, Partial<PropMap[K]>>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K] | Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {\n const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {\n type: \"ui\" as const,\n id: message?.id ?? uuidv4(),\n name: message?.name,\n props: message?.props,\n metadata: {\n merge: options?.merge || undefined,\n run_id: runId,\n tags: config.tags,\n name: config.runName,\n ...message?.metadata,\n ...(options?.message ? { id: options.message.id } : null),\n },\n };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n }\n\n const handleDelete = (id: string): RemoveUIMessage => {\n const evt: RemoveUIMessage = { type: \"remove-ui\", id };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n };\n\n return { push: handlePush, delete: handleDelete, items };\n};\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,WACX,QAWA,YAIG;CAEH,MAAM,QAAyC,EAAE;CACjD,MAAM,WAAW,SAAS,YAAY;CAEtC,MAAM,QAAS,OAAO,UAAU,UAAiC,OAAO;AACxE,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,qBAAqB;CAsBjD,SAAS,WACP,SAMA,SACgD;EAChD,MAAM,MAAsD;GAC1D,MAAM;GACN,IAAI,SAAS,OAAA,GAAA,2BAAA,KAAc;GAC3B,MAAM,SAAS;GACf,OAAO,SAAS;GAChB,UAAU;IACR,OAAO,SAAS,SAAS,KAAA;IACzB,QAAQ;IACR,MAAM,OAAO;IACb,MAAM,OAAO;IACb,GAAG,SAAS;IACZ,GAAI,SAAS,UAAU,EAAE,IAAI,QAAQ,QAAQ,IAAI,GAAG;IACrD;GACF;AACD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;CAGT,MAAM,gBAAgB,OAAgC;EACpD,MAAM,MAAuB;GAAE,MAAM;GAAa;GAAI;AACtD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;AAGT,QAAO;EAAE,MAAM;EAAY,QAAQ;EAAc;EAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["uuidv4"],"sources":["../../../src/react-ui/server/server.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"uuid\";\nimport type { ComponentPropsWithoutRef, ElementType } from \"react\";\nimport type { RemoveUIMessage, UIMessage } from \"../types.js\";\n\ninterface MessageLike {\n id?: string;\n}\n\n/**\n * Helper to send and persist UI messages. Accepts a map of component names to React components\n * as type argument to provide type safety. Will also write to the `options?.stateKey` state.\n *\n * @param config LangGraphRunnableConfig\n * @param options\n * @returns\n */\nexport const typedUi = <Decl extends Record<string, ElementType>>(\n config: {\n writer?: (chunk: unknown) => void;\n runId?: string;\n metadata?: Record<string, unknown>;\n tags?: string[];\n runName?: string;\n configurable?: {\n __pregel_send?: (writes_: [string, unknown][]) => void;\n [key: string]: unknown;\n };\n },\n options?: {\n /** The key to write the UI messages to. Defaults to `ui`. */\n stateKey?: string;\n }\n) => {\n type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };\n const items: (UIMessage | RemoveUIMessage)[] = [];\n const stateKey = options?.stateKey ?? \"ui\";\n\n const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;\n if (!runId) throw new Error(\"run_id is required\");\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K];\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K]>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options: { message?: MessageLike; merge: true }\n ): UIMessage<K, Partial<PropMap[K]>>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K] | Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {\n const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {\n type: \"ui\" as const,\n id: message?.id ?? uuidv4(),\n name: message?.name,\n props: message?.props,\n metadata: {\n merge: options?.merge || undefined,\n run_id: runId,\n tags: config.tags,\n name: config.runName,\n ...message?.metadata,\n ...(options?.message ? { id: options.message.id } : null),\n },\n };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n }\n\n const handleDelete = (id: string): RemoveUIMessage => {\n const evt: RemoveUIMessage = { type: \"remove-ui\", id };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n };\n\n return { push: handlePush, delete: handleDelete, items };\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,WACX,QAWA,YAIG;CAEH,MAAM,QAAyC,EAAE;CACjD,MAAM,WAAW,SAAS,YAAY;CAEtC,MAAM,QAAS,OAAO,UAAU,UAAiC,OAAO;AACxE,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,qBAAqB;CAsBjD,SAAS,WACP,SAMA,SACgD;EAChD,MAAM,MAAsD;GAC1D,MAAM;GACN,IAAI,SAAS,MAAMA,IAAQ;GAC3B,MAAM,SAAS;GACf,OAAO,SAAS;GAChB,UAAU;IACR,OAAO,SAAS,SAAS,KAAA;IACzB,QAAQ;IACR,MAAM,OAAO;IACb,MAAM,OAAO;IACb,GAAG,SAAS;IACZ,GAAI,SAAS,UAAU,EAAE,IAAI,QAAQ,QAAQ,IAAI,GAAG;IACrD;GACF;AACD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;CAGT,MAAM,gBAAgB,OAAgC;EACpD,MAAM,MAAuB;GAAE,MAAM;GAAa;GAAI;AACtD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;AAGT,QAAO;EAAE,MAAM;EAAY,QAAQ;EAAc;EAAO"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["uuidv4"],"sources":["../../../src/react-ui/server/server.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"@langchain/core/utils/uuid\";\nimport type { ComponentPropsWithoutRef, ElementType } from \"react\";\nimport type { RemoveUIMessage, UIMessage } from \"../types.js\";\n\ninterface MessageLike {\n id?: string;\n}\n\n/**\n * Helper to send and persist UI messages. Accepts a map of component names to React components\n * as type argument to provide type safety. Will also write to the `options?.stateKey` state.\n *\n * @param config LangGraphRunnableConfig\n * @param options\n * @returns\n */\nexport const typedUi = <Decl extends Record<string, ElementType>>(\n config: {\n writer?: (chunk: unknown) => void;\n runId?: string;\n metadata?: Record<string, unknown>;\n tags?: string[];\n runName?: string;\n configurable?: {\n __pregel_send?: (writes_: [string, unknown][]) => void;\n [key: string]: unknown;\n };\n },\n options?: {\n /** The key to write the UI messages to. Defaults to `ui`. */\n stateKey?: string;\n }\n) => {\n type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };\n const items: (UIMessage | RemoveUIMessage)[] = [];\n const stateKey = options?.stateKey ?? \"ui\";\n\n const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;\n if (!runId) throw new Error(\"run_id is required\");\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K];\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K]>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options: { message?: MessageLike; merge: true }\n ): UIMessage<K, Partial<PropMap[K]>>;\n\n function handlePush<K extends keyof PropMap & string>(\n message: {\n id?: string;\n name: K;\n props: PropMap[K] | Partial<PropMap[K]>;\n metadata?: Record<string, unknown>;\n },\n options?: { message?: MessageLike; merge?: boolean }\n ): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {\n const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {\n type: \"ui\" as const,\n id: message?.id ?? uuidv4(),\n name: message?.name,\n props: message?.props,\n metadata: {\n merge: options?.merge || undefined,\n run_id: runId,\n tags: config.tags,\n name: config.runName,\n ...message?.metadata,\n ...(options?.message ? { id: options.message.id } : null),\n },\n };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n }\n\n const handleDelete = (id: string): RemoveUIMessage => {\n const evt: RemoveUIMessage = { type: \"remove-ui\", id };\n items.push(evt);\n config.writer?.(evt);\n config.configurable?.__pregel_send?.([[stateKey, evt]]);\n return evt;\n };\n\n return { push: handlePush, delete: handleDelete, items };\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,WACX,QAWA,YAIG;CAEH,MAAM,QAAyC,EAAE;CACjD,MAAM,WAAW,SAAS,YAAY;CAEtC,MAAM,QAAS,OAAO,UAAU,UAAiC,OAAO;AACxE,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,qBAAqB;CAsBjD,SAAS,WACP,SAMA,SACgD;EAChD,MAAM,MAAsD;GAC1D,MAAM;GACN,IAAI,SAAS,MAAMA,IAAQ;GAC3B,MAAM,SAAS;GACf,OAAO,SAAS;GAChB,UAAU;IACR,OAAO,SAAS,SAAS,KAAA;IACzB,QAAQ;IACR,MAAM,OAAO;IACb,MAAM,OAAO;IACb,GAAG,SAAS;IACZ,GAAI,SAAS,UAAU,EAAE,IAAI,QAAQ,QAAQ,IAAI,GAAG;IACrD;GACF;AACD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;CAGT,MAAM,gBAAgB,OAAgC;EACpD,MAAM,MAAuB;GAAE,MAAM;GAAa;GAAI;AACtD,QAAM,KAAK,IAAI;AACf,SAAO,SAAS,IAAI;AACpB,SAAO,cAAc,gBAAgB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;AACvD,SAAO;;AAGT,QAAO;EAAE,MAAM;EAAY,QAAQ;EAAc;EAAO"}
|
|
@@ -151,7 +151,8 @@ var ChannelRegistry = class {
|
|
|
151
151
|
initial: spec.initial,
|
|
152
152
|
open: spec.open,
|
|
153
153
|
refCount: 0,
|
|
154
|
-
runtime: void 0
|
|
154
|
+
runtime: void 0,
|
|
155
|
+
pendingDispose: void 0
|
|
155
156
|
};
|
|
156
157
|
if (this.#thread != null) newEntry.runtime = spec.open({
|
|
157
158
|
thread: this.#thread,
|
|
@@ -161,6 +162,7 @@ var ChannelRegistry = class {
|
|
|
161
162
|
this.#entries.set(spec.key, newEntry);
|
|
162
163
|
entry = newEntry;
|
|
163
164
|
}
|
|
165
|
+
entry.pendingDispose = void 0;
|
|
164
166
|
entry.refCount += 1;
|
|
165
167
|
let released = false;
|
|
166
168
|
return {
|
|
@@ -172,8 +174,16 @@ var ChannelRegistry = class {
|
|
|
172
174
|
if (current == null) return;
|
|
173
175
|
current.refCount -= 1;
|
|
174
176
|
if (current.refCount <= 0) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
current.refCount = 0;
|
|
178
|
+
const token = {};
|
|
179
|
+
current.pendingDispose = token;
|
|
180
|
+
queueMicrotask(() => {
|
|
181
|
+
const latest = this.#entries.get(spec.key);
|
|
182
|
+
if (latest == null || latest !== current || latest.pendingDispose !== token || latest.refCount > 0) return;
|
|
183
|
+
this.#entries.delete(spec.key);
|
|
184
|
+
latest.pendingDispose = void 0;
|
|
185
|
+
if (latest.runtime != null) tryDispose(latest.runtime);
|
|
186
|
+
});
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
};
|
|
@@ -195,12 +205,15 @@ var ChannelRegistry = class {
|
|
|
195
205
|
}));
|
|
196
206
|
}
|
|
197
207
|
/**
|
|
198
|
-
* Number of
|
|
199
|
-
* branch on this value at runtime; it exists for tests asserting
|
|
200
|
-
* that consumers properly release their projections.
|
|
208
|
+
* Number of actively-held entries. Diagnostic-only — callers should
|
|
209
|
+
* not branch on this value at runtime; it exists for tests asserting
|
|
210
|
+
* that consumers properly release their projections. Entries waiting
|
|
211
|
+
* on cancellable microtask disposal do not count as active.
|
|
201
212
|
*/
|
|
202
213
|
get size() {
|
|
203
|
-
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const entry of this.#entries.values()) if (entry.refCount > 0) count += 1;
|
|
216
|
+
return count;
|
|
204
217
|
}
|
|
205
218
|
};
|
|
206
219
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-registry.cjs","names":["#rootBus","#entries","#thread","StreamStore"],"sources":["../../src/stream/channel-registry.ts"],"sourcesContent":["/**\n * Framework-agnostic ref-counted subscription cache.\n *\n * # What this module is\n *\n * Every framework binding (React, Vue, Svelte, Angular) owns one\n * {@link ChannelRegistry} per {@link StreamController}. The registry\n * is the single layer that:\n *\n * 1. Deduplicates server-side subscriptions across components — N\n * hooks reading the same projection share one\n * `thread.subscribe(...)` call and one {@link StreamStore}.\n * 2. Lazily opens / tears down subscriptions in step with mounting\n * and unmounting consumers (ref counting on `spec.key`).\n * 3. Survives thread swaps — `controller.hydrate(newThreadId)`\n * rebinds every live entry against the new thread without\n * changing store identity, so React's\n * `useSyncExternalStore` (and equivalents in other frameworks)\n * keep working.\n *\n * # Why ref counting matters\n *\n * Most projections back at least one server subscription. Without\n * deduplication, every additional consumer of e.g. `useMessages(sub)`\n * would open its own SSE/WebSocket subscription, paying the same\n * payload N times. The registry guarantees we only ever pay once per\n * `spec.key`, regardless of how many consumers attach.\n *\n * # Why store identity is preserved on rebind\n *\n * Framework reactivity primitives subscribe to a store *instance* and\n * memoise their last seen snapshot. If we minted a new store on every\n * thread swap, every bound component would silently lose its\n * subscription. Instead, the registry keeps the same {@link StreamStore}\n * but resets its value to `spec.initial` and re-runs `spec.open()` —\n * consumers observe a clean slate without re-subscribing.\n *\n * @see ProjectionSpec - The contract every projection implements.\n * @see StreamStore - The observable store handed to consumers.\n */\nimport { StreamStore } from \"./store.js\";\nimport type {\n AcquiredProjection,\n ProjectionRuntime,\n ProjectionSpec,\n RootEventBus,\n ThreadStream,\n} from \"./types.js\";\n\n/**\n * Internal record kept for each unique `spec.key` actively held by at\n * least one consumer.\n *\n * We intentionally store `initial` and `open` separately from `spec`\n * so the registry never depends on the spec object's identity — two\n * specs sharing the same `key` but produced from different factory\n * calls (e.g. fresh objects on each render) still collapse onto the\n * same entry.\n */\ninterface Entry {\n /** Stable identity used for deduplication. */\n readonly key: string;\n /** Observable store handed back to every consumer of this key. */\n readonly store: StreamStore<unknown>;\n /** Initial snapshot reapplied on dispose / thread rebind. */\n readonly initial: unknown;\n /** Factory that opens the underlying subscription against a thread. */\n readonly open: ProjectionSpec<unknown>[\"open\"];\n /** Live consumers of this entry. Drops to 0 → entry is torn down. */\n refCount: number;\n /**\n * Active runtime returned by `open()`. Undefined while detached\n * (no thread bound yet, or a rebind is in progress).\n */\n runtime: ProjectionRuntime | undefined;\n}\n\n/**\n * Ref-counted, thread-aware projection registry.\n *\n * Owns the `spec.key → (store, runtime)` mapping for one\n * {@link StreamController}. Lifecycle:\n *\n * - `acquire(spec)` → +1 ref, returns `{ store, release }`. The\n * first acquire opens the projection's runtime; subsequent\n * acquires for the same key share both the store and the\n * runtime.\n * - `release()` → -1 ref. When the last consumer releases,\n * the entry is removed and its runtime disposed.\n * - `bind(thread)` → swap or detach the underlying thread; every\n * live entry's runtime is recreated against the new thread,\n * keeping the same store identity.\n * - `dispose()` → tear everything down (idempotent). Safe to\n * call multiple times.\n *\n * The registry is intentionally not generic over a state shape —\n * different consumers can hold projections producing different\n * snapshot types, so the registry keys everything as `unknown` and\n * lets {@link acquire} reapply the caller's `T` at the boundary.\n */\nexport class ChannelRegistry {\n /** Currently bound thread, or `undefined` while detached. */\n #thread: ThreadStream | undefined;\n\n /** Read-only fan-out of the controller's root subscription. */\n readonly #rootBus: RootEventBus;\n\n /** All live entries, keyed by `spec.key`. */\n readonly #entries = new Map<string, Entry>();\n\n /**\n * Construct a registry bound to the controller's root event bus.\n *\n * The bus is forwarded to every projection's `open()` so root-scoped\n * projections can avoid opening a second server subscription when\n * their channel set is already covered by the root pump.\n *\n * @param rootBus - Read-only fan-out of the root subscription.\n */\n constructor(rootBus: RootEventBus) {\n this.#rootBus = rootBus;\n }\n\n /**\n * Rebind every live entry to a new {@link ThreadStream} (or detach\n * when `thread == null`).\n *\n * Each live entry has its current runtime disposed (best-effort)\n * and its store reset to `entry.initial` so consumers see a clean\n * slate during the swap. When `thread != null`, a fresh runtime is\n * opened against the new thread.\n *\n * Critically the {@link StreamStore} *instance* is preserved across\n * the rebind: framework subscribers (e.g. React's\n * `useSyncExternalStore`) keep observing the same store reference,\n * so their subscriptions survive the swap.\n *\n * No-op when called with the currently bound thread.\n *\n * @param thread - The thread stream to bind, or `undefined` to detach.\n */\n bind(thread: ThreadStream | undefined): void {\n if (this.#thread === thread) return;\n const previous = this.#thread;\n this.#thread = thread;\n for (const entry of this.#entries.values()) {\n // Tear down any active runtime from the previous thread.\n if (entry.runtime != null && previous != null) {\n void tryDispose(entry.runtime);\n }\n entry.runtime = undefined;\n entry.store.setValue(entry.initial);\n if (thread != null) {\n entry.runtime = entry.open({\n thread,\n store: entry.store,\n rootBus: this.#rootBus,\n });\n }\n }\n }\n\n /** Currently bound thread (may be `undefined` pre-mount). */\n get thread(): ThreadStream | undefined {\n return this.#thread;\n }\n\n /**\n * Acquire a ref-counted projection.\n *\n * If no entry exists for `spec.key`, one is created (allocating a\n * {@link StreamStore} seeded with `spec.initial`) and — when a\n * thread is currently bound — its runtime is opened immediately.\n * If an entry already exists, its ref count is incremented and the\n * existing store is returned.\n *\n * The returned `release()` is idempotent: calling it more than once\n * is a no-op. When the ref count drops to zero, the entry is removed\n * and its runtime disposed (best-effort, never throws into callers).\n *\n * Safe to call from any framework lifecycle hook. Subsequent calls\n * for the same `spec.key` always return the same `store` reference\n * for the lifetime of the controller, so consumers can rely on store\n * identity.\n *\n * @typeParam T - Snapshot type produced by this projection.\n * @param spec - Projection contract; the registry keys off `spec.key`.\n * @returns A `{ store, release }` handle.\n */\n acquire<T>(spec: ProjectionSpec<T>): AcquiredProjection<T> {\n let entry = this.#entries.get(spec.key);\n if (entry == null) {\n const store = new StreamStore<T>(spec.initial);\n const newEntry: Entry = {\n key: spec.key,\n store: store as StreamStore<unknown>,\n initial: spec.initial as unknown,\n open: spec.open as ProjectionSpec<unknown>[\"open\"],\n refCount: 0,\n runtime: undefined,\n };\n // Open the runtime immediately when a thread is already bound.\n // Otherwise it will be opened lazily by the next `bind()` call.\n if (this.#thread != null) {\n newEntry.runtime = spec.open({\n thread: this.#thread,\n store,\n rootBus: this.#rootBus,\n });\n }\n this.#entries.set(spec.key, newEntry);\n entry = newEntry;\n }\n entry.refCount += 1;\n\n let released = false;\n return {\n store: entry.store as StreamStore<T>,\n release: () => {\n if (released) return;\n released = true;\n const current = this.#entries.get(spec.key);\n if (current == null) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n this.#entries.delete(spec.key);\n if (current.runtime != null) void tryDispose(current.runtime);\n }\n },\n };\n }\n\n /**\n * Tear everything down.\n *\n * Detaches the bound thread (so no further `bind()` calls reopen\n * runtimes) and disposes every live runtime in parallel. Safe to\n * call multiple times — subsequent calls find an empty registry\n * and resolve immediately.\n */\n async dispose(): Promise<void> {\n this.#thread = undefined;\n const entries = [...this.#entries.values()];\n this.#entries.clear();\n await Promise.all(\n entries.map(async (entry) => {\n if (entry.runtime != null) await tryDispose(entry.runtime);\n })\n );\n }\n\n /**\n * Number of live entries. Diagnostic-only — callers should not\n * branch on this value at runtime; it exists for tests asserting\n * that consumers properly release their projections.\n */\n get size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * Best-effort runtime disposal.\n *\n * `dispose()` should never throw, but a misbehaving projection should\n * not be able to wedge the entire registry. We swallow disposal\n * errors so the surrounding `bind()` / `release()` / `dispose()`\n * paths always make progress.\n *\n * @param runtime - Runtime returned by {@link ProjectionSpec.open}.\n */\nasync function tryDispose(runtime: ProjectionRuntime): Promise<void> {\n try {\n await runtime.dispose();\n } catch {\n // Best-effort — dispose should never throw, but we don't want a\n // bad projection to wedge the registry.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoGA,IAAa,kBAAb,MAA6B;;CAE3B;;CAGA;;CAGA,2BAAoB,IAAI,KAAoB;;;;;;;;;;CAW5C,YAAY,SAAuB;AACjC,QAAA,UAAgB;;;;;;;;;;;;;;;;;;;;CAqBlB,KAAK,QAAwC;AAC3C,MAAI,MAAA,WAAiB,OAAQ;EAC7B,MAAM,WAAW,MAAA;AACjB,QAAA,SAAe;AACf,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,EAAE;AAE1C,OAAI,MAAM,WAAW,QAAQ,YAAY,KAClC,YAAW,MAAM,QAAQ;AAEhC,SAAM,UAAU,KAAA;AAChB,SAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,OAAI,UAAU,KACZ,OAAM,UAAU,MAAM,KAAK;IACzB;IACA,OAAO,MAAM;IACb,SAAS,MAAA;IACV,CAAC;;;;CAMR,IAAI,SAAmC;AACrC,SAAO,MAAA;;;;;;;;;;;;;;;;;;;;;;;;CAyBT,QAAW,MAAgD;EACzD,IAAI,QAAQ,MAAA,QAAc,IAAI,KAAK,IAAI;AACvC,MAAI,SAAS,MAAM;GACjB,MAAM,QAAQ,IAAIG,cAAAA,YAAe,KAAK,QAAQ;GAC9C,MAAM,WAAkB;IACtB,KAAK,KAAK;IACH;IACP,SAAS,KAAK;IACd,MAAM,KAAK;IACX,UAAU;IACV,SAAS,KAAA;IACV;AAGD,OAAI,MAAA,UAAgB,KAClB,UAAS,UAAU,KAAK,KAAK;IAC3B,QAAQ,MAAA;IACR;IACA,SAAS,MAAA;IACV,CAAC;AAEJ,SAAA,QAAc,IAAI,KAAK,KAAK,SAAS;AACrC,WAAQ;;AAEV,QAAM,YAAY;EAElB,IAAI,WAAW;AACf,SAAO;GACL,OAAO,MAAM;GACb,eAAe;AACb,QAAI,SAAU;AACd,eAAW;IACX,MAAM,UAAU,MAAA,QAAc,IAAI,KAAK,IAAI;AAC3C,QAAI,WAAW,KAAM;AACrB,YAAQ,YAAY;AACpB,QAAI,QAAQ,YAAY,GAAG;AACzB,WAAA,QAAc,OAAO,KAAK,IAAI;AAC9B,SAAI,QAAQ,WAAW,KAAW,YAAW,QAAQ,QAAQ;;;GAGlE;;;;;;;;;;CAWH,MAAM,UAAyB;AAC7B,QAAA,SAAe,KAAA;EACf,MAAM,UAAU,CAAC,GAAG,MAAA,QAAc,QAAQ,CAAC;AAC3C,QAAA,QAAc,OAAO;AACrB,QAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,UAAU;AAC3B,OAAI,MAAM,WAAW,KAAM,OAAM,WAAW,MAAM,QAAQ;IAC1D,CACH;;;;;;;CAQH,IAAI,OAAe;AACjB,SAAO,MAAA,QAAc;;;;;;;;;;;;;AAczB,eAAe,WAAW,SAA2C;AACnE,KAAI;AACF,QAAM,QAAQ,SAAS;SACjB"}
|
|
1
|
+
{"version":3,"file":"channel-registry.cjs","names":["#rootBus","#entries","#thread","StreamStore"],"sources":["../../src/stream/channel-registry.ts"],"sourcesContent":["/**\n * Framework-agnostic ref-counted subscription cache.\n *\n * # What this module is\n *\n * Every framework binding (React, Vue, Svelte, Angular) owns one\n * {@link ChannelRegistry} per {@link StreamController}. The registry\n * is the single layer that:\n *\n * 1. Deduplicates server-side subscriptions across components — N\n * hooks reading the same projection share one\n * `thread.subscribe(...)` call and one {@link StreamStore}.\n * 2. Lazily opens / tears down subscriptions in step with mounting\n * and unmounting consumers (ref counting on `spec.key`).\n * 3. Survives thread swaps — `controller.hydrate(newThreadId)`\n * rebinds every live entry against the new thread without\n * changing store identity, so React's\n * `useSyncExternalStore` (and equivalents in other frameworks)\n * keep working.\n *\n * # Why ref counting matters\n *\n * Most projections back at least one server subscription. Without\n * deduplication, every additional consumer of e.g. `useMessages(sub)`\n * would open its own SSE/WebSocket subscription, paying the same\n * payload N times. The registry guarantees we only ever pay once per\n * `spec.key`, regardless of how many consumers attach.\n *\n * # Why store identity is preserved on rebind\n *\n * Framework reactivity primitives subscribe to a store *instance* and\n * memoise their last seen snapshot. If we minted a new store on every\n * thread swap, every bound component would silently lose its\n * subscription. Instead, the registry keeps the same {@link StreamStore}\n * but resets its value to `spec.initial` and re-runs `spec.open()` —\n * consumers observe a clean slate without re-subscribing.\n *\n * @see ProjectionSpec - The contract every projection implements.\n * @see StreamStore - The observable store handed to consumers.\n */\nimport { StreamStore } from \"./store.js\";\nimport type {\n AcquiredProjection,\n ProjectionRuntime,\n ProjectionSpec,\n RootEventBus,\n ThreadStream,\n} from \"./types.js\";\n\n/**\n * Internal record kept for each unique `spec.key` actively held by at\n * least one consumer.\n *\n * We intentionally store `initial` and `open` separately from `spec`\n * so the registry never depends on the spec object's identity — two\n * specs sharing the same `key` but produced from different factory\n * calls (e.g. fresh objects on each render) still collapse onto the\n * same entry.\n */\ninterface Entry {\n /** Stable identity used for deduplication. */\n readonly key: string;\n /** Observable store handed back to every consumer of this key. */\n readonly store: StreamStore<unknown>;\n /** Initial snapshot reapplied on dispose / thread rebind. */\n readonly initial: unknown;\n /** Factory that opens the underlying subscription against a thread. */\n readonly open: ProjectionSpec<unknown>[\"open\"];\n /** Live consumers of this entry. Drops to 0 → entry is torn down. */\n refCount: number;\n /**\n * Active runtime returned by `open()`. Undefined while detached\n * (no thread bound yet, or a rebind is in progress).\n */\n runtime: ProjectionRuntime | undefined;\n /**\n * Token for a deferred last-release disposal. Re-acquiring the same\n * projection before the microtask runs clears the token and keeps the\n * runtime alive.\n */\n pendingDispose: object | undefined;\n}\n\n/**\n * Ref-counted, thread-aware projection registry.\n *\n * Owns the `spec.key → (store, runtime)` mapping for one\n * {@link StreamController}. Lifecycle:\n *\n * - `acquire(spec)` → +1 ref, returns `{ store, release }`. The\n * first acquire opens the projection's runtime; subsequent\n * acquires for the same key share both the store and the\n * runtime.\n * - `release()` → -1 ref. When the last consumer releases,\n * the entry is removed and its runtime disposed.\n * - `bind(thread)` → swap or detach the underlying thread; every\n * live entry's runtime is recreated against the new thread,\n * keeping the same store identity.\n * - `dispose()` → tear everything down (idempotent). Safe to\n * call multiple times.\n *\n * The registry is intentionally not generic over a state shape —\n * different consumers can hold projections producing different\n * snapshot types, so the registry keys everything as `unknown` and\n * lets {@link acquire} reapply the caller's `T` at the boundary.\n */\nexport class ChannelRegistry {\n /** Currently bound thread, or `undefined` while detached. */\n #thread: ThreadStream | undefined;\n\n /** Read-only fan-out of the controller's root subscription. */\n readonly #rootBus: RootEventBus;\n\n /** All live entries, keyed by `spec.key`. */\n readonly #entries = new Map<string, Entry>();\n\n /**\n * Construct a registry bound to the controller's root event bus.\n *\n * The bus is forwarded to every projection's `open()` so root-scoped\n * projections can avoid opening a second server subscription when\n * their channel set is already covered by the root pump.\n *\n * @param rootBus - Read-only fan-out of the root subscription.\n */\n constructor(rootBus: RootEventBus) {\n this.#rootBus = rootBus;\n }\n\n /**\n * Rebind every live entry to a new {@link ThreadStream} (or detach\n * when `thread == null`).\n *\n * Each live entry has its current runtime disposed (best-effort)\n * and its store reset to `entry.initial` so consumers see a clean\n * slate during the swap. When `thread != null`, a fresh runtime is\n * opened against the new thread.\n *\n * Critically the {@link StreamStore} *instance* is preserved across\n * the rebind: framework subscribers (e.g. React's\n * `useSyncExternalStore`) keep observing the same store reference,\n * so their subscriptions survive the swap.\n *\n * No-op when called with the currently bound thread.\n *\n * @param thread - The thread stream to bind, or `undefined` to detach.\n */\n bind(thread: ThreadStream | undefined): void {\n if (this.#thread === thread) return;\n const previous = this.#thread;\n this.#thread = thread;\n for (const entry of this.#entries.values()) {\n // Tear down any active runtime from the previous thread.\n if (entry.runtime != null && previous != null) {\n void tryDispose(entry.runtime);\n }\n entry.runtime = undefined;\n entry.store.setValue(entry.initial);\n if (thread != null) {\n entry.runtime = entry.open({\n thread,\n store: entry.store,\n rootBus: this.#rootBus,\n });\n }\n }\n }\n\n /** Currently bound thread (may be `undefined` pre-mount). */\n get thread(): ThreadStream | undefined {\n return this.#thread;\n }\n\n /**\n * Acquire a ref-counted projection.\n *\n * If no entry exists for `spec.key`, one is created (allocating a\n * {@link StreamStore} seeded with `spec.initial`) and — when a\n * thread is currently bound — its runtime is opened immediately.\n * If an entry already exists, its ref count is incremented and the\n * existing store is returned.\n *\n * The returned `release()` is idempotent: calling it more than once\n * is a no-op. When the ref count drops to zero, the entry is removed\n * and its runtime disposed (best-effort, never throws into callers).\n *\n * Safe to call from any framework lifecycle hook. Subsequent calls\n * for the same `spec.key` always return the same `store` reference\n * for the lifetime of the controller, so consumers can rely on store\n * identity.\n *\n * @typeParam T - Snapshot type produced by this projection.\n * @param spec - Projection contract; the registry keys off `spec.key`.\n * @returns A `{ store, release }` handle.\n */\n acquire<T>(spec: ProjectionSpec<T>): AcquiredProjection<T> {\n let entry = this.#entries.get(spec.key);\n if (entry == null) {\n const store = new StreamStore<T>(spec.initial);\n const newEntry: Entry = {\n key: spec.key,\n store: store as StreamStore<unknown>,\n initial: spec.initial as unknown,\n open: spec.open as ProjectionSpec<unknown>[\"open\"],\n refCount: 0,\n runtime: undefined,\n pendingDispose: undefined,\n };\n // Open the runtime immediately when a thread is already bound.\n // Otherwise it will be opened lazily by the next `bind()` call.\n if (this.#thread != null) {\n newEntry.runtime = spec.open({\n thread: this.#thread,\n store,\n rootBus: this.#rootBus,\n });\n }\n this.#entries.set(spec.key, newEntry);\n entry = newEntry;\n }\n entry.pendingDispose = undefined;\n entry.refCount += 1;\n\n let released = false;\n return {\n store: entry.store as StreamStore<T>,\n release: () => {\n if (released) return;\n released = true;\n const current = this.#entries.get(spec.key);\n if (current == null) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.refCount = 0;\n const token = {};\n current.pendingDispose = token;\n queueMicrotask(() => {\n const latest = this.#entries.get(spec.key);\n if (\n latest == null ||\n latest !== current ||\n latest.pendingDispose !== token ||\n latest.refCount > 0\n ) {\n return;\n }\n this.#entries.delete(spec.key);\n latest.pendingDispose = undefined;\n if (latest.runtime != null) void tryDispose(latest.runtime);\n });\n }\n },\n };\n }\n\n /**\n * Tear everything down.\n *\n * Detaches the bound thread (so no further `bind()` calls reopen\n * runtimes) and disposes every live runtime in parallel. Safe to\n * call multiple times — subsequent calls find an empty registry\n * and resolve immediately.\n */\n async dispose(): Promise<void> {\n this.#thread = undefined;\n const entries = [...this.#entries.values()];\n this.#entries.clear();\n await Promise.all(\n entries.map(async (entry) => {\n if (entry.runtime != null) await tryDispose(entry.runtime);\n })\n );\n }\n\n /**\n * Number of actively-held entries. Diagnostic-only — callers should\n * not branch on this value at runtime; it exists for tests asserting\n * that consumers properly release their projections. Entries waiting\n * on cancellable microtask disposal do not count as active.\n */\n get size(): number {\n let count = 0;\n for (const entry of this.#entries.values()) {\n if (entry.refCount > 0) count += 1;\n }\n return count;\n }\n}\n\n/**\n * Best-effort runtime disposal.\n *\n * `dispose()` should never throw, but a misbehaving projection should\n * not be able to wedge the entire registry. We swallow disposal\n * errors so the surrounding `bind()` / `release()` / `dispose()`\n * paths always make progress.\n *\n * @param runtime - Runtime returned by {@link ProjectionSpec.open}.\n */\nasync function tryDispose(runtime: ProjectionRuntime): Promise<void> {\n try {\n await runtime.dispose();\n } catch {\n // Best-effort — dispose should never throw, but we don't want a\n // bad projection to wedge the registry.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0GA,IAAa,kBAAb,MAA6B;;CAE3B;;CAGA;;CAGA,2BAAoB,IAAI,KAAoB;;;;;;;;;;CAW5C,YAAY,SAAuB;AACjC,QAAA,UAAgB;;;;;;;;;;;;;;;;;;;;CAqBlB,KAAK,QAAwC;AAC3C,MAAI,MAAA,WAAiB,OAAQ;EAC7B,MAAM,WAAW,MAAA;AACjB,QAAA,SAAe;AACf,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,EAAE;AAE1C,OAAI,MAAM,WAAW,QAAQ,YAAY,KAClC,YAAW,MAAM,QAAQ;AAEhC,SAAM,UAAU,KAAA;AAChB,SAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,OAAI,UAAU,KACZ,OAAM,UAAU,MAAM,KAAK;IACzB;IACA,OAAO,MAAM;IACb,SAAS,MAAA;IACV,CAAC;;;;CAMR,IAAI,SAAmC;AACrC,SAAO,MAAA;;;;;;;;;;;;;;;;;;;;;;;;CAyBT,QAAW,MAAgD;EACzD,IAAI,QAAQ,MAAA,QAAc,IAAI,KAAK,IAAI;AACvC,MAAI,SAAS,MAAM;GACjB,MAAM,QAAQ,IAAIG,cAAAA,YAAe,KAAK,QAAQ;GAC9C,MAAM,WAAkB;IACtB,KAAK,KAAK;IACH;IACP,SAAS,KAAK;IACd,MAAM,KAAK;IACX,UAAU;IACV,SAAS,KAAA;IACT,gBAAgB,KAAA;IACjB;AAGD,OAAI,MAAA,UAAgB,KAClB,UAAS,UAAU,KAAK,KAAK;IAC3B,QAAQ,MAAA;IACR;IACA,SAAS,MAAA;IACV,CAAC;AAEJ,SAAA,QAAc,IAAI,KAAK,KAAK,SAAS;AACrC,WAAQ;;AAEV,QAAM,iBAAiB,KAAA;AACvB,QAAM,YAAY;EAElB,IAAI,WAAW;AACf,SAAO;GACL,OAAO,MAAM;GACb,eAAe;AACb,QAAI,SAAU;AACd,eAAW;IACX,MAAM,UAAU,MAAA,QAAc,IAAI,KAAK,IAAI;AAC3C,QAAI,WAAW,KAAM;AACrB,YAAQ,YAAY;AACpB,QAAI,QAAQ,YAAY,GAAG;AACzB,aAAQ,WAAW;KACnB,MAAM,QAAQ,EAAE;AAChB,aAAQ,iBAAiB;AACzB,0BAAqB;MACnB,MAAM,SAAS,MAAA,QAAc,IAAI,KAAK,IAAI;AAC1C,UACE,UAAU,QACV,WAAW,WACX,OAAO,mBAAmB,SAC1B,OAAO,WAAW,EAElB;AAEF,YAAA,QAAc,OAAO,KAAK,IAAI;AAC9B,aAAO,iBAAiB,KAAA;AACxB,UAAI,OAAO,WAAW,KAAW,YAAW,OAAO,QAAQ;OAC3D;;;GAGP;;;;;;;;;;CAWH,MAAM,UAAyB;AAC7B,QAAA,SAAe,KAAA;EACf,MAAM,UAAU,CAAC,GAAG,MAAA,QAAc,QAAQ,CAAC;AAC3C,QAAA,QAAc,OAAO;AACrB,QAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,UAAU;AAC3B,OAAI,MAAM,WAAW,KAAM,OAAM,WAAW,MAAM,QAAQ;IAC1D,CACH;;;;;;;;CASH,IAAI,OAAe;EACjB,IAAI,QAAQ;AACZ,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,CACxC,KAAI,MAAM,WAAW,EAAG,UAAS;AAEnC,SAAO;;;;;;;;;;;;;AAcX,eAAe,WAAW,SAA2C;AACnE,KAAI;AACF,QAAM,QAAQ,SAAS;SACjB"}
|
|
@@ -91,9 +91,10 @@ declare class ChannelRegistry {
|
|
|
91
91
|
*/
|
|
92
92
|
dispose(): Promise<void>;
|
|
93
93
|
/**
|
|
94
|
-
* Number of
|
|
95
|
-
* branch on this value at runtime; it exists for tests asserting
|
|
96
|
-
* that consumers properly release their projections.
|
|
94
|
+
* Number of actively-held entries. Diagnostic-only — callers should
|
|
95
|
+
* not branch on this value at runtime; it exists for tests asserting
|
|
96
|
+
* that consumers properly release their projections. Entries waiting
|
|
97
|
+
* on cancellable microtask disposal do not count as active.
|
|
97
98
|
*/
|
|
98
99
|
get size(): number;
|
|
99
100
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-registry.d.cts","names":[],"sources":["../../src/stream/channel-registry.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"channel-registry.d.cts","names":[],"sources":["../../src/stream/channel-registry.ts"],"mappings":";;;;;;;AA0GA;;;;;;;;;;;;;;;;;;;;cAAa,eAAA;EAAA;EAyFX;;;;;;;;;EAtEA,WAAA,CAAY,OAAA,EAAS,YAAA;EA2Jb;;;;;;;;;;;;;;;;;;EArIR,IAAA,CAAK,MAAA,EAAQ,YAAA;;MAsBT,MAAA,CAAA,GAAU,YAAA;;;;;;;;;;;;;;;;;;;;;;;EA0Bd,OAAA,GAAA,CAAW,IAAA,EAAM,cAAA,CAAe,CAAA,IAAK,kBAAA,CAAmB,CAAA;;;;;;;;;EAoElD,OAAA,CAAA,GAAW,OAAA;;;;;;;MAiBb,IAAA,CAAA;AAAA"}
|
|
@@ -91,9 +91,10 @@ declare class ChannelRegistry {
|
|
|
91
91
|
*/
|
|
92
92
|
dispose(): Promise<void>;
|
|
93
93
|
/**
|
|
94
|
-
* Number of
|
|
95
|
-
* branch on this value at runtime; it exists for tests asserting
|
|
96
|
-
* that consumers properly release their projections.
|
|
94
|
+
* Number of actively-held entries. Diagnostic-only — callers should
|
|
95
|
+
* not branch on this value at runtime; it exists for tests asserting
|
|
96
|
+
* that consumers properly release their projections. Entries waiting
|
|
97
|
+
* on cancellable microtask disposal do not count as active.
|
|
97
98
|
*/
|
|
98
99
|
get size(): number;
|
|
99
100
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-registry.d.ts","names":[],"sources":["../../src/stream/channel-registry.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"channel-registry.d.ts","names":[],"sources":["../../src/stream/channel-registry.ts"],"mappings":";;;;;;;AA0GA;;;;;;;;;;;;;;;;;;;;cAAa,eAAA;EAAA;EAyFX;;;;;;;;;EAtEA,WAAA,CAAY,OAAA,EAAS,YAAA;EA2Jb;;;;;;;;;;;;;;;;;;EArIR,IAAA,CAAK,MAAA,EAAQ,YAAA;;MAsBT,MAAA,CAAA,GAAU,YAAA;;;;;;;;;;;;;;;;;;;;;;;EA0Bd,OAAA,GAAA,CAAW,IAAA,EAAM,cAAA,CAAe,CAAA,IAAK,kBAAA,CAAmB,CAAA;;;;;;;;;EAoElD,OAAA,CAAA,GAAW,OAAA;;;;;;;MAiBb,IAAA,CAAA;AAAA"}
|
|
@@ -151,7 +151,8 @@ var ChannelRegistry = class {
|
|
|
151
151
|
initial: spec.initial,
|
|
152
152
|
open: spec.open,
|
|
153
153
|
refCount: 0,
|
|
154
|
-
runtime: void 0
|
|
154
|
+
runtime: void 0,
|
|
155
|
+
pendingDispose: void 0
|
|
155
156
|
};
|
|
156
157
|
if (this.#thread != null) newEntry.runtime = spec.open({
|
|
157
158
|
thread: this.#thread,
|
|
@@ -161,6 +162,7 @@ var ChannelRegistry = class {
|
|
|
161
162
|
this.#entries.set(spec.key, newEntry);
|
|
162
163
|
entry = newEntry;
|
|
163
164
|
}
|
|
165
|
+
entry.pendingDispose = void 0;
|
|
164
166
|
entry.refCount += 1;
|
|
165
167
|
let released = false;
|
|
166
168
|
return {
|
|
@@ -172,8 +174,16 @@ var ChannelRegistry = class {
|
|
|
172
174
|
if (current == null) return;
|
|
173
175
|
current.refCount -= 1;
|
|
174
176
|
if (current.refCount <= 0) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
current.refCount = 0;
|
|
178
|
+
const token = {};
|
|
179
|
+
current.pendingDispose = token;
|
|
180
|
+
queueMicrotask(() => {
|
|
181
|
+
const latest = this.#entries.get(spec.key);
|
|
182
|
+
if (latest == null || latest !== current || latest.pendingDispose !== token || latest.refCount > 0) return;
|
|
183
|
+
this.#entries.delete(spec.key);
|
|
184
|
+
latest.pendingDispose = void 0;
|
|
185
|
+
if (latest.runtime != null) tryDispose(latest.runtime);
|
|
186
|
+
});
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
};
|
|
@@ -195,12 +205,15 @@ var ChannelRegistry = class {
|
|
|
195
205
|
}));
|
|
196
206
|
}
|
|
197
207
|
/**
|
|
198
|
-
* Number of
|
|
199
|
-
* branch on this value at runtime; it exists for tests asserting
|
|
200
|
-
* that consumers properly release their projections.
|
|
208
|
+
* Number of actively-held entries. Diagnostic-only — callers should
|
|
209
|
+
* not branch on this value at runtime; it exists for tests asserting
|
|
210
|
+
* that consumers properly release their projections. Entries waiting
|
|
211
|
+
* on cancellable microtask disposal do not count as active.
|
|
201
212
|
*/
|
|
202
213
|
get size() {
|
|
203
|
-
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const entry of this.#entries.values()) if (entry.refCount > 0) count += 1;
|
|
216
|
+
return count;
|
|
204
217
|
}
|
|
205
218
|
};
|
|
206
219
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-registry.js","names":["#rootBus","#entries","#thread"],"sources":["../../src/stream/channel-registry.ts"],"sourcesContent":["/**\n * Framework-agnostic ref-counted subscription cache.\n *\n * # What this module is\n *\n * Every framework binding (React, Vue, Svelte, Angular) owns one\n * {@link ChannelRegistry} per {@link StreamController}. The registry\n * is the single layer that:\n *\n * 1. Deduplicates server-side subscriptions across components — N\n * hooks reading the same projection share one\n * `thread.subscribe(...)` call and one {@link StreamStore}.\n * 2. Lazily opens / tears down subscriptions in step with mounting\n * and unmounting consumers (ref counting on `spec.key`).\n * 3. Survives thread swaps — `controller.hydrate(newThreadId)`\n * rebinds every live entry against the new thread without\n * changing store identity, so React's\n * `useSyncExternalStore` (and equivalents in other frameworks)\n * keep working.\n *\n * # Why ref counting matters\n *\n * Most projections back at least one server subscription. Without\n * deduplication, every additional consumer of e.g. `useMessages(sub)`\n * would open its own SSE/WebSocket subscription, paying the same\n * payload N times. The registry guarantees we only ever pay once per\n * `spec.key`, regardless of how many consumers attach.\n *\n * # Why store identity is preserved on rebind\n *\n * Framework reactivity primitives subscribe to a store *instance* and\n * memoise their last seen snapshot. If we minted a new store on every\n * thread swap, every bound component would silently lose its\n * subscription. Instead, the registry keeps the same {@link StreamStore}\n * but resets its value to `spec.initial` and re-runs `spec.open()` —\n * consumers observe a clean slate without re-subscribing.\n *\n * @see ProjectionSpec - The contract every projection implements.\n * @see StreamStore - The observable store handed to consumers.\n */\nimport { StreamStore } from \"./store.js\";\nimport type {\n AcquiredProjection,\n ProjectionRuntime,\n ProjectionSpec,\n RootEventBus,\n ThreadStream,\n} from \"./types.js\";\n\n/**\n * Internal record kept for each unique `spec.key` actively held by at\n * least one consumer.\n *\n * We intentionally store `initial` and `open` separately from `spec`\n * so the registry never depends on the spec object's identity — two\n * specs sharing the same `key` but produced from different factory\n * calls (e.g. fresh objects on each render) still collapse onto the\n * same entry.\n */\ninterface Entry {\n /** Stable identity used for deduplication. */\n readonly key: string;\n /** Observable store handed back to every consumer of this key. */\n readonly store: StreamStore<unknown>;\n /** Initial snapshot reapplied on dispose / thread rebind. */\n readonly initial: unknown;\n /** Factory that opens the underlying subscription against a thread. */\n readonly open: ProjectionSpec<unknown>[\"open\"];\n /** Live consumers of this entry. Drops to 0 → entry is torn down. */\n refCount: number;\n /**\n * Active runtime returned by `open()`. Undefined while detached\n * (no thread bound yet, or a rebind is in progress).\n */\n runtime: ProjectionRuntime | undefined;\n}\n\n/**\n * Ref-counted, thread-aware projection registry.\n *\n * Owns the `spec.key → (store, runtime)` mapping for one\n * {@link StreamController}. Lifecycle:\n *\n * - `acquire(spec)` → +1 ref, returns `{ store, release }`. The\n * first acquire opens the projection's runtime; subsequent\n * acquires for the same key share both the store and the\n * runtime.\n * - `release()` → -1 ref. When the last consumer releases,\n * the entry is removed and its runtime disposed.\n * - `bind(thread)` → swap or detach the underlying thread; every\n * live entry's runtime is recreated against the new thread,\n * keeping the same store identity.\n * - `dispose()` → tear everything down (idempotent). Safe to\n * call multiple times.\n *\n * The registry is intentionally not generic over a state shape —\n * different consumers can hold projections producing different\n * snapshot types, so the registry keys everything as `unknown` and\n * lets {@link acquire} reapply the caller's `T` at the boundary.\n */\nexport class ChannelRegistry {\n /** Currently bound thread, or `undefined` while detached. */\n #thread: ThreadStream | undefined;\n\n /** Read-only fan-out of the controller's root subscription. */\n readonly #rootBus: RootEventBus;\n\n /** All live entries, keyed by `spec.key`. */\n readonly #entries = new Map<string, Entry>();\n\n /**\n * Construct a registry bound to the controller's root event bus.\n *\n * The bus is forwarded to every projection's `open()` so root-scoped\n * projections can avoid opening a second server subscription when\n * their channel set is already covered by the root pump.\n *\n * @param rootBus - Read-only fan-out of the root subscription.\n */\n constructor(rootBus: RootEventBus) {\n this.#rootBus = rootBus;\n }\n\n /**\n * Rebind every live entry to a new {@link ThreadStream} (or detach\n * when `thread == null`).\n *\n * Each live entry has its current runtime disposed (best-effort)\n * and its store reset to `entry.initial` so consumers see a clean\n * slate during the swap. When `thread != null`, a fresh runtime is\n * opened against the new thread.\n *\n * Critically the {@link StreamStore} *instance* is preserved across\n * the rebind: framework subscribers (e.g. React's\n * `useSyncExternalStore`) keep observing the same store reference,\n * so their subscriptions survive the swap.\n *\n * No-op when called with the currently bound thread.\n *\n * @param thread - The thread stream to bind, or `undefined` to detach.\n */\n bind(thread: ThreadStream | undefined): void {\n if (this.#thread === thread) return;\n const previous = this.#thread;\n this.#thread = thread;\n for (const entry of this.#entries.values()) {\n // Tear down any active runtime from the previous thread.\n if (entry.runtime != null && previous != null) {\n void tryDispose(entry.runtime);\n }\n entry.runtime = undefined;\n entry.store.setValue(entry.initial);\n if (thread != null) {\n entry.runtime = entry.open({\n thread,\n store: entry.store,\n rootBus: this.#rootBus,\n });\n }\n }\n }\n\n /** Currently bound thread (may be `undefined` pre-mount). */\n get thread(): ThreadStream | undefined {\n return this.#thread;\n }\n\n /**\n * Acquire a ref-counted projection.\n *\n * If no entry exists for `spec.key`, one is created (allocating a\n * {@link StreamStore} seeded with `spec.initial`) and — when a\n * thread is currently bound — its runtime is opened immediately.\n * If an entry already exists, its ref count is incremented and the\n * existing store is returned.\n *\n * The returned `release()` is idempotent: calling it more than once\n * is a no-op. When the ref count drops to zero, the entry is removed\n * and its runtime disposed (best-effort, never throws into callers).\n *\n * Safe to call from any framework lifecycle hook. Subsequent calls\n * for the same `spec.key` always return the same `store` reference\n * for the lifetime of the controller, so consumers can rely on store\n * identity.\n *\n * @typeParam T - Snapshot type produced by this projection.\n * @param spec - Projection contract; the registry keys off `spec.key`.\n * @returns A `{ store, release }` handle.\n */\n acquire<T>(spec: ProjectionSpec<T>): AcquiredProjection<T> {\n let entry = this.#entries.get(spec.key);\n if (entry == null) {\n const store = new StreamStore<T>(spec.initial);\n const newEntry: Entry = {\n key: spec.key,\n store: store as StreamStore<unknown>,\n initial: spec.initial as unknown,\n open: spec.open as ProjectionSpec<unknown>[\"open\"],\n refCount: 0,\n runtime: undefined,\n };\n // Open the runtime immediately when a thread is already bound.\n // Otherwise it will be opened lazily by the next `bind()` call.\n if (this.#thread != null) {\n newEntry.runtime = spec.open({\n thread: this.#thread,\n store,\n rootBus: this.#rootBus,\n });\n }\n this.#entries.set(spec.key, newEntry);\n entry = newEntry;\n }\n entry.refCount += 1;\n\n let released = false;\n return {\n store: entry.store as StreamStore<T>,\n release: () => {\n if (released) return;\n released = true;\n const current = this.#entries.get(spec.key);\n if (current == null) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n this.#entries.delete(spec.key);\n if (current.runtime != null) void tryDispose(current.runtime);\n }\n },\n };\n }\n\n /**\n * Tear everything down.\n *\n * Detaches the bound thread (so no further `bind()` calls reopen\n * runtimes) and disposes every live runtime in parallel. Safe to\n * call multiple times — subsequent calls find an empty registry\n * and resolve immediately.\n */\n async dispose(): Promise<void> {\n this.#thread = undefined;\n const entries = [...this.#entries.values()];\n this.#entries.clear();\n await Promise.all(\n entries.map(async (entry) => {\n if (entry.runtime != null) await tryDispose(entry.runtime);\n })\n );\n }\n\n /**\n * Number of live entries. Diagnostic-only — callers should not\n * branch on this value at runtime; it exists for tests asserting\n * that consumers properly release their projections.\n */\n get size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * Best-effort runtime disposal.\n *\n * `dispose()` should never throw, but a misbehaving projection should\n * not be able to wedge the entire registry. We swallow disposal\n * errors so the surrounding `bind()` / `release()` / `dispose()`\n * paths always make progress.\n *\n * @param runtime - Runtime returned by {@link ProjectionSpec.open}.\n */\nasync function tryDispose(runtime: ProjectionRuntime): Promise<void> {\n try {\n await runtime.dispose();\n } catch {\n // Best-effort — dispose should never throw, but we don't want a\n // bad projection to wedge the registry.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoGA,IAAa,kBAAb,MAA6B;;CAE3B;;CAGA;;CAGA,2BAAoB,IAAI,KAAoB;;;;;;;;;;CAW5C,YAAY,SAAuB;AACjC,QAAA,UAAgB;;;;;;;;;;;;;;;;;;;;CAqBlB,KAAK,QAAwC;AAC3C,MAAI,MAAA,WAAiB,OAAQ;EAC7B,MAAM,WAAW,MAAA;AACjB,QAAA,SAAe;AACf,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,EAAE;AAE1C,OAAI,MAAM,WAAW,QAAQ,YAAY,KAClC,YAAW,MAAM,QAAQ;AAEhC,SAAM,UAAU,KAAA;AAChB,SAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,OAAI,UAAU,KACZ,OAAM,UAAU,MAAM,KAAK;IACzB;IACA,OAAO,MAAM;IACb,SAAS,MAAA;IACV,CAAC;;;;CAMR,IAAI,SAAmC;AACrC,SAAO,MAAA;;;;;;;;;;;;;;;;;;;;;;;;CAyBT,QAAW,MAAgD;EACzD,IAAI,QAAQ,MAAA,QAAc,IAAI,KAAK,IAAI;AACvC,MAAI,SAAS,MAAM;GACjB,MAAM,QAAQ,IAAI,YAAe,KAAK,QAAQ;GAC9C,MAAM,WAAkB;IACtB,KAAK,KAAK;IACH;IACP,SAAS,KAAK;IACd,MAAM,KAAK;IACX,UAAU;IACV,SAAS,KAAA;IACV;AAGD,OAAI,MAAA,UAAgB,KAClB,UAAS,UAAU,KAAK,KAAK;IAC3B,QAAQ,MAAA;IACR;IACA,SAAS,MAAA;IACV,CAAC;AAEJ,SAAA,QAAc,IAAI,KAAK,KAAK,SAAS;AACrC,WAAQ;;AAEV,QAAM,YAAY;EAElB,IAAI,WAAW;AACf,SAAO;GACL,OAAO,MAAM;GACb,eAAe;AACb,QAAI,SAAU;AACd,eAAW;IACX,MAAM,UAAU,MAAA,QAAc,IAAI,KAAK,IAAI;AAC3C,QAAI,WAAW,KAAM;AACrB,YAAQ,YAAY;AACpB,QAAI,QAAQ,YAAY,GAAG;AACzB,WAAA,QAAc,OAAO,KAAK,IAAI;AAC9B,SAAI,QAAQ,WAAW,KAAW,YAAW,QAAQ,QAAQ;;;GAGlE;;;;;;;;;;CAWH,MAAM,UAAyB;AAC7B,QAAA,SAAe,KAAA;EACf,MAAM,UAAU,CAAC,GAAG,MAAA,QAAc,QAAQ,CAAC;AAC3C,QAAA,QAAc,OAAO;AACrB,QAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,UAAU;AAC3B,OAAI,MAAM,WAAW,KAAM,OAAM,WAAW,MAAM,QAAQ;IAC1D,CACH;;;;;;;CAQH,IAAI,OAAe;AACjB,SAAO,MAAA,QAAc;;;;;;;;;;;;;AAczB,eAAe,WAAW,SAA2C;AACnE,KAAI;AACF,QAAM,QAAQ,SAAS;SACjB"}
|
|
1
|
+
{"version":3,"file":"channel-registry.js","names":["#rootBus","#entries","#thread"],"sources":["../../src/stream/channel-registry.ts"],"sourcesContent":["/**\n * Framework-agnostic ref-counted subscription cache.\n *\n * # What this module is\n *\n * Every framework binding (React, Vue, Svelte, Angular) owns one\n * {@link ChannelRegistry} per {@link StreamController}. The registry\n * is the single layer that:\n *\n * 1. Deduplicates server-side subscriptions across components — N\n * hooks reading the same projection share one\n * `thread.subscribe(...)` call and one {@link StreamStore}.\n * 2. Lazily opens / tears down subscriptions in step with mounting\n * and unmounting consumers (ref counting on `spec.key`).\n * 3. Survives thread swaps — `controller.hydrate(newThreadId)`\n * rebinds every live entry against the new thread without\n * changing store identity, so React's\n * `useSyncExternalStore` (and equivalents in other frameworks)\n * keep working.\n *\n * # Why ref counting matters\n *\n * Most projections back at least one server subscription. Without\n * deduplication, every additional consumer of e.g. `useMessages(sub)`\n * would open its own SSE/WebSocket subscription, paying the same\n * payload N times. The registry guarantees we only ever pay once per\n * `spec.key`, regardless of how many consumers attach.\n *\n * # Why store identity is preserved on rebind\n *\n * Framework reactivity primitives subscribe to a store *instance* and\n * memoise their last seen snapshot. If we minted a new store on every\n * thread swap, every bound component would silently lose its\n * subscription. Instead, the registry keeps the same {@link StreamStore}\n * but resets its value to `spec.initial` and re-runs `spec.open()` —\n * consumers observe a clean slate without re-subscribing.\n *\n * @see ProjectionSpec - The contract every projection implements.\n * @see StreamStore - The observable store handed to consumers.\n */\nimport { StreamStore } from \"./store.js\";\nimport type {\n AcquiredProjection,\n ProjectionRuntime,\n ProjectionSpec,\n RootEventBus,\n ThreadStream,\n} from \"./types.js\";\n\n/**\n * Internal record kept for each unique `spec.key` actively held by at\n * least one consumer.\n *\n * We intentionally store `initial` and `open` separately from `spec`\n * so the registry never depends on the spec object's identity — two\n * specs sharing the same `key` but produced from different factory\n * calls (e.g. fresh objects on each render) still collapse onto the\n * same entry.\n */\ninterface Entry {\n /** Stable identity used for deduplication. */\n readonly key: string;\n /** Observable store handed back to every consumer of this key. */\n readonly store: StreamStore<unknown>;\n /** Initial snapshot reapplied on dispose / thread rebind. */\n readonly initial: unknown;\n /** Factory that opens the underlying subscription against a thread. */\n readonly open: ProjectionSpec<unknown>[\"open\"];\n /** Live consumers of this entry. Drops to 0 → entry is torn down. */\n refCount: number;\n /**\n * Active runtime returned by `open()`. Undefined while detached\n * (no thread bound yet, or a rebind is in progress).\n */\n runtime: ProjectionRuntime | undefined;\n /**\n * Token for a deferred last-release disposal. Re-acquiring the same\n * projection before the microtask runs clears the token and keeps the\n * runtime alive.\n */\n pendingDispose: object | undefined;\n}\n\n/**\n * Ref-counted, thread-aware projection registry.\n *\n * Owns the `spec.key → (store, runtime)` mapping for one\n * {@link StreamController}. Lifecycle:\n *\n * - `acquire(spec)` → +1 ref, returns `{ store, release }`. The\n * first acquire opens the projection's runtime; subsequent\n * acquires for the same key share both the store and the\n * runtime.\n * - `release()` → -1 ref. When the last consumer releases,\n * the entry is removed and its runtime disposed.\n * - `bind(thread)` → swap or detach the underlying thread; every\n * live entry's runtime is recreated against the new thread,\n * keeping the same store identity.\n * - `dispose()` → tear everything down (idempotent). Safe to\n * call multiple times.\n *\n * The registry is intentionally not generic over a state shape —\n * different consumers can hold projections producing different\n * snapshot types, so the registry keys everything as `unknown` and\n * lets {@link acquire} reapply the caller's `T` at the boundary.\n */\nexport class ChannelRegistry {\n /** Currently bound thread, or `undefined` while detached. */\n #thread: ThreadStream | undefined;\n\n /** Read-only fan-out of the controller's root subscription. */\n readonly #rootBus: RootEventBus;\n\n /** All live entries, keyed by `spec.key`. */\n readonly #entries = new Map<string, Entry>();\n\n /**\n * Construct a registry bound to the controller's root event bus.\n *\n * The bus is forwarded to every projection's `open()` so root-scoped\n * projections can avoid opening a second server subscription when\n * their channel set is already covered by the root pump.\n *\n * @param rootBus - Read-only fan-out of the root subscription.\n */\n constructor(rootBus: RootEventBus) {\n this.#rootBus = rootBus;\n }\n\n /**\n * Rebind every live entry to a new {@link ThreadStream} (or detach\n * when `thread == null`).\n *\n * Each live entry has its current runtime disposed (best-effort)\n * and its store reset to `entry.initial` so consumers see a clean\n * slate during the swap. When `thread != null`, a fresh runtime is\n * opened against the new thread.\n *\n * Critically the {@link StreamStore} *instance* is preserved across\n * the rebind: framework subscribers (e.g. React's\n * `useSyncExternalStore`) keep observing the same store reference,\n * so their subscriptions survive the swap.\n *\n * No-op when called with the currently bound thread.\n *\n * @param thread - The thread stream to bind, or `undefined` to detach.\n */\n bind(thread: ThreadStream | undefined): void {\n if (this.#thread === thread) return;\n const previous = this.#thread;\n this.#thread = thread;\n for (const entry of this.#entries.values()) {\n // Tear down any active runtime from the previous thread.\n if (entry.runtime != null && previous != null) {\n void tryDispose(entry.runtime);\n }\n entry.runtime = undefined;\n entry.store.setValue(entry.initial);\n if (thread != null) {\n entry.runtime = entry.open({\n thread,\n store: entry.store,\n rootBus: this.#rootBus,\n });\n }\n }\n }\n\n /** Currently bound thread (may be `undefined` pre-mount). */\n get thread(): ThreadStream | undefined {\n return this.#thread;\n }\n\n /**\n * Acquire a ref-counted projection.\n *\n * If no entry exists for `spec.key`, one is created (allocating a\n * {@link StreamStore} seeded with `spec.initial`) and — when a\n * thread is currently bound — its runtime is opened immediately.\n * If an entry already exists, its ref count is incremented and the\n * existing store is returned.\n *\n * The returned `release()` is idempotent: calling it more than once\n * is a no-op. When the ref count drops to zero, the entry is removed\n * and its runtime disposed (best-effort, never throws into callers).\n *\n * Safe to call from any framework lifecycle hook. Subsequent calls\n * for the same `spec.key` always return the same `store` reference\n * for the lifetime of the controller, so consumers can rely on store\n * identity.\n *\n * @typeParam T - Snapshot type produced by this projection.\n * @param spec - Projection contract; the registry keys off `spec.key`.\n * @returns A `{ store, release }` handle.\n */\n acquire<T>(spec: ProjectionSpec<T>): AcquiredProjection<T> {\n let entry = this.#entries.get(spec.key);\n if (entry == null) {\n const store = new StreamStore<T>(spec.initial);\n const newEntry: Entry = {\n key: spec.key,\n store: store as StreamStore<unknown>,\n initial: spec.initial as unknown,\n open: spec.open as ProjectionSpec<unknown>[\"open\"],\n refCount: 0,\n runtime: undefined,\n pendingDispose: undefined,\n };\n // Open the runtime immediately when a thread is already bound.\n // Otherwise it will be opened lazily by the next `bind()` call.\n if (this.#thread != null) {\n newEntry.runtime = spec.open({\n thread: this.#thread,\n store,\n rootBus: this.#rootBus,\n });\n }\n this.#entries.set(spec.key, newEntry);\n entry = newEntry;\n }\n entry.pendingDispose = undefined;\n entry.refCount += 1;\n\n let released = false;\n return {\n store: entry.store as StreamStore<T>,\n release: () => {\n if (released) return;\n released = true;\n const current = this.#entries.get(spec.key);\n if (current == null) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.refCount = 0;\n const token = {};\n current.pendingDispose = token;\n queueMicrotask(() => {\n const latest = this.#entries.get(spec.key);\n if (\n latest == null ||\n latest !== current ||\n latest.pendingDispose !== token ||\n latest.refCount > 0\n ) {\n return;\n }\n this.#entries.delete(spec.key);\n latest.pendingDispose = undefined;\n if (latest.runtime != null) void tryDispose(latest.runtime);\n });\n }\n },\n };\n }\n\n /**\n * Tear everything down.\n *\n * Detaches the bound thread (so no further `bind()` calls reopen\n * runtimes) and disposes every live runtime in parallel. Safe to\n * call multiple times — subsequent calls find an empty registry\n * and resolve immediately.\n */\n async dispose(): Promise<void> {\n this.#thread = undefined;\n const entries = [...this.#entries.values()];\n this.#entries.clear();\n await Promise.all(\n entries.map(async (entry) => {\n if (entry.runtime != null) await tryDispose(entry.runtime);\n })\n );\n }\n\n /**\n * Number of actively-held entries. Diagnostic-only — callers should\n * not branch on this value at runtime; it exists for tests asserting\n * that consumers properly release their projections. Entries waiting\n * on cancellable microtask disposal do not count as active.\n */\n get size(): number {\n let count = 0;\n for (const entry of this.#entries.values()) {\n if (entry.refCount > 0) count += 1;\n }\n return count;\n }\n}\n\n/**\n * Best-effort runtime disposal.\n *\n * `dispose()` should never throw, but a misbehaving projection should\n * not be able to wedge the entire registry. We swallow disposal\n * errors so the surrounding `bind()` / `release()` / `dispose()`\n * paths always make progress.\n *\n * @param runtime - Runtime returned by {@link ProjectionSpec.open}.\n */\nasync function tryDispose(runtime: ProjectionRuntime): Promise<void> {\n try {\n await runtime.dispose();\n } catch {\n // Best-effort — dispose should never throw, but we don't want a\n // bad projection to wedge the registry.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0GA,IAAa,kBAAb,MAA6B;;CAE3B;;CAGA;;CAGA,2BAAoB,IAAI,KAAoB;;;;;;;;;;CAW5C,YAAY,SAAuB;AACjC,QAAA,UAAgB;;;;;;;;;;;;;;;;;;;;CAqBlB,KAAK,QAAwC;AAC3C,MAAI,MAAA,WAAiB,OAAQ;EAC7B,MAAM,WAAW,MAAA;AACjB,QAAA,SAAe;AACf,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,EAAE;AAE1C,OAAI,MAAM,WAAW,QAAQ,YAAY,KAClC,YAAW,MAAM,QAAQ;AAEhC,SAAM,UAAU,KAAA;AAChB,SAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,OAAI,UAAU,KACZ,OAAM,UAAU,MAAM,KAAK;IACzB;IACA,OAAO,MAAM;IACb,SAAS,MAAA;IACV,CAAC;;;;CAMR,IAAI,SAAmC;AACrC,SAAO,MAAA;;;;;;;;;;;;;;;;;;;;;;;;CAyBT,QAAW,MAAgD;EACzD,IAAI,QAAQ,MAAA,QAAc,IAAI,KAAK,IAAI;AACvC,MAAI,SAAS,MAAM;GACjB,MAAM,QAAQ,IAAI,YAAe,KAAK,QAAQ;GAC9C,MAAM,WAAkB;IACtB,KAAK,KAAK;IACH;IACP,SAAS,KAAK;IACd,MAAM,KAAK;IACX,UAAU;IACV,SAAS,KAAA;IACT,gBAAgB,KAAA;IACjB;AAGD,OAAI,MAAA,UAAgB,KAClB,UAAS,UAAU,KAAK,KAAK;IAC3B,QAAQ,MAAA;IACR;IACA,SAAS,MAAA;IACV,CAAC;AAEJ,SAAA,QAAc,IAAI,KAAK,KAAK,SAAS;AACrC,WAAQ;;AAEV,QAAM,iBAAiB,KAAA;AACvB,QAAM,YAAY;EAElB,IAAI,WAAW;AACf,SAAO;GACL,OAAO,MAAM;GACb,eAAe;AACb,QAAI,SAAU;AACd,eAAW;IACX,MAAM,UAAU,MAAA,QAAc,IAAI,KAAK,IAAI;AAC3C,QAAI,WAAW,KAAM;AACrB,YAAQ,YAAY;AACpB,QAAI,QAAQ,YAAY,GAAG;AACzB,aAAQ,WAAW;KACnB,MAAM,QAAQ,EAAE;AAChB,aAAQ,iBAAiB;AACzB,0BAAqB;MACnB,MAAM,SAAS,MAAA,QAAc,IAAI,KAAK,IAAI;AAC1C,UACE,UAAU,QACV,WAAW,WACX,OAAO,mBAAmB,SAC1B,OAAO,WAAW,EAElB;AAEF,YAAA,QAAc,OAAO,KAAK,IAAI;AAC9B,aAAO,iBAAiB,KAAA;AACxB,UAAI,OAAO,WAAW,KAAW,YAAW,OAAO,QAAQ;OAC3D;;;GAGP;;;;;;;;;;CAWH,MAAM,UAAyB;AAC7B,QAAA,SAAe,KAAA;EACf,MAAM,UAAU,CAAC,GAAG,MAAA,QAAc,QAAQ,CAAC;AAC3C,QAAA,QAAc,OAAO;AACrB,QAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,UAAU;AAC3B,OAAI,MAAM,WAAW,KAAM,OAAM,WAAW,MAAM,QAAQ;IAC1D,CACH;;;;;;;;CASH,IAAI,OAAe;EACjB,IAAI,QAAQ;AACZ,OAAK,MAAM,SAAS,MAAA,QAAc,QAAQ,CACxC,KAAI,MAAM,WAAW,EAAG,UAAS;AAEnC,SAAO;;;;;;;;;;;;;AAcX,eAAe,WAAW,SAA2C;AACnE,KAAI;AACF,QAAM,QAAQ,SAAS;SACjB"}
|
|
@@ -18,7 +18,7 @@ const require_root_message_projection = require("./root-message-projection.cjs")
|
|
|
18
18
|
const require_optimistic_input = require("./optimistic-input.cjs");
|
|
19
19
|
const require_submit_coordinator = require("./submit-coordinator.cjs");
|
|
20
20
|
const require_tool_calls = require("./tool-calls.cjs");
|
|
21
|
-
let
|
|
21
|
+
let _langchain_core_utils_uuid = require("@langchain/core/utils/uuid");
|
|
22
22
|
require("@langchain/core/messages");
|
|
23
23
|
//#region src/stream/controller.ts
|
|
24
24
|
/**
|
|
@@ -1529,7 +1529,7 @@ var StreamController = class {
|
|
|
1529
1529
|
#beginOptimistic(input) {
|
|
1530
1530
|
if (this.#options.optimistic === false) return void 0;
|
|
1531
1531
|
if (input == null || typeof input !== "object" || Array.isArray(input)) return;
|
|
1532
|
-
const prepared = require_optimistic_input.prepareOptimisticInput(input, this.#messagesKey, () => (0,
|
|
1532
|
+
const prepared = require_optimistic_input.prepareOptimisticInput(input, this.#messagesKey, () => (0, _langchain_core_utils_uuid.v7)());
|
|
1533
1533
|
const extraKeys = Object.keys(prepared.extraValues);
|
|
1534
1534
|
if (prepared.echoedIds.length === 0 && extraKeys.length === 0) return;
|
|
1535
1535
|
const currentValues = this.rootStore.getSnapshot().values;
|