@truefoundry/trueforge-core 0.2.1 → 0.3.0-rc.0

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.
Files changed (55) hide show
  1. package/dist/core/llm/LLMTypes.js +2 -2
  2. package/dist/core/llm/LLMTypes.js.map +1 -1
  3. package/dist/core/llm/LLMTypes.mjs +2 -2
  4. package/dist/core/llm/LLMTypes.mjs.map +1 -1
  5. package/dist/core/llm/VercelAILLM.d.ts.map +1 -1
  6. package/dist/core/llm/VercelAILLM.js +3 -0
  7. package/dist/core/llm/VercelAILLM.js.map +1 -1
  8. package/dist/core/llm/VercelAILLM.mjs +3 -0
  9. package/dist/core/llm/VercelAILLM.mjs.map +1 -1
  10. package/dist/core/llm/toOpenAIChatMessage.d.ts +4 -3
  11. package/dist/core/llm/toOpenAIChatMessage.d.ts.map +1 -1
  12. package/dist/core/llm/toOpenAIChatMessage.js.map +1 -1
  13. package/dist/core/llm/toOpenAIChatMessage.mjs.map +1 -1
  14. package/dist/core/runtime/AgentThread.d.ts.map +1 -1
  15. package/dist/core/runtime/AgentThread.js +7 -1
  16. package/dist/core/runtime/AgentThread.js.map +1 -1
  17. package/dist/core/runtime/AgentThread.mjs +7 -1
  18. package/dist/core/runtime/AgentThread.mjs.map +1 -1
  19. package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts.map +1 -1
  20. package/dist/core/sandbox/provider/TFYSandboxProvider.js +43 -14
  21. package/dist/core/sandbox/provider/TFYSandboxProvider.js.map +1 -1
  22. package/dist/core/sandbox/provider/TFYSandboxProvider.mjs +44 -15
  23. package/dist/core/sandbox/provider/TFYSandboxProvider.mjs.map +1 -1
  24. package/dist/core/util/errorLogFields.d.ts +1 -0
  25. package/dist/core/util/errorLogFields.d.ts.map +1 -1
  26. package/dist/core/util/errorLogFields.js +33 -6
  27. package/dist/core/util/errorLogFields.js.map +1 -1
  28. package/dist/core/util/errorLogFields.mjs +33 -6
  29. package/dist/core/util/errorLogFields.mjs.map +1 -1
  30. package/dist/core/web-search/ParallelWebSearchProvider.d.ts.map +1 -1
  31. package/dist/core/web-search/ParallelWebSearchProvider.js +5 -1
  32. package/dist/core/web-search/ParallelWebSearchProvider.js.map +1 -1
  33. package/dist/core/web-search/ParallelWebSearchProvider.mjs +5 -1
  34. package/dist/core/web-search/ParallelWebSearchProvider.mjs.map +1 -1
  35. package/dist/request-reply/client.d.ts +2 -2
  36. package/dist/request-reply/client.d.ts.map +1 -1
  37. package/dist/request-reply/client.js.map +1 -1
  38. package/dist/request-reply/client.mjs.map +1 -1
  39. package/dist/request-reply/executor.d.ts +3 -3
  40. package/dist/request-reply/executor.d.ts.map +1 -1
  41. package/dist/request-reply/executor.js +0 -1
  42. package/dist/request-reply/executor.js.map +1 -1
  43. package/dist/request-reply/executor.mjs +0 -1
  44. package/dist/request-reply/executor.mjs.map +1 -1
  45. package/dist/request-reply/index.d.ts +1 -0
  46. package/dist/request-reply/index.d.ts.map +1 -1
  47. package/dist/request-reply/index.js.map +1 -1
  48. package/dist/request-reply/index.mjs.map +1 -1
  49. package/dist/request-reply/redisClient.d.ts +4 -0
  50. package/dist/request-reply/redisClient.d.ts.map +1 -0
  51. package/dist/request-reply/redisClient.js +19 -0
  52. package/dist/request-reply/redisClient.js.map +1 -0
  53. package/dist/request-reply/redisClient.mjs +1 -0
  54. package/dist/request-reply/redisClient.mjs.map +1 -0
  55. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/request-reply/executor.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { RedisClientType } from 'redis';\nimport type { Logger } from 'winston';\nimport z from 'zod';\nimport { extractErrorLogFields } from '../core/util/errorLogFields';\nimport { ReplyError } from './errors';\nimport type { JSONReply, RequestHandler } from './types';\nimport { publishedRequestSchema } from './types';\nimport { heartbeatKey, requestChannel } from './utils';\n\nexport interface RunExecutorOptions {\n /** How often to refresh the heartbeat key (ms). */\n heartbeatIntervalMs: number;\n /** TTL for the reply value so abandoned keys are reclaimed (ms). */\n replyTtlMs: number;\n}\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;\nconst DEFAULT_REPLY_TTL_MS = 120_000;\n\nfunction resolveRunExecutorOptions(options: Partial<RunExecutorOptions> | undefined): RunExecutorOptions {\n return {\n heartbeatIntervalMs: options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,\n replyTtlMs: options?.replyTtlMs ?? DEFAULT_REPLY_TTL_MS,\n };\n}\n\n/** Observability hook for subscriber errors, failed SUBSCRIBEs, and malformed messages. */\nexport type RequestReplyErrorHandler = (err: Error, context: { executorId: string; channel: string }) => void;\n\n/**\n * Serving side of the request-reply transport: parses and validates each\n * published message, dispatches it to the request handler, writes the\n * JSONReply to the request's reply key, and refreshes the heartbeat key while\n * subscribed.\n *\n * Connection lifecycle stays with the caller: both clients are supplied\n * connected and are never closed here. `subscriberClient` is the connection to\n * SUBSCRIBE on — a `duplicate()` for standalone Redis, or the shared client\n * for Sentinel — and must have a reconnect strategy so a failed connect\n * self-heals (the executor re-subscribes on every `ready`).\n */\nexport class RequestReplyExecutor {\n readonly executorId: string;\n /** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */\n readonly channel: string;\n private readonly redis: RedisClientType;\n private readonly subscriberClient: RedisClientType;\n private readonly logger: Logger;\n private readonly heartbeatIntervalMs: number;\n private readonly heartbeatTtlMs: number;\n private readonly replyTtlMs: number;\n private readonly heartbeatKey: string;\n private readonly requestHandler: RequestHandler;\n private readonly onError: RequestReplyErrorHandler | undefined;\n /** Stable reference so node-redis dedupes the listener across re-subscribes. */\n private readonly subscribeListener = (message: string): void => {\n this.onMessage(message);\n };\n\n private closed = false;\n private initialized = false;\n private runningRequests = new Map<string, Promise<void>>();\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor({\n executorId,\n redis,\n subscriberClient,\n requestHandler,\n onError,\n logger,\n options,\n }: {\n executorId: string;\n /** Connected command client, used only for SET (reply + heartbeat). Caller owns its lifecycle. */\n redis: RedisClientType;\n /** Connected client to SUBSCRIBE on (duplicate or Sentinel). Caller owns its lifecycle. */\n subscriberClient: RedisClientType;\n requestHandler: RequestHandler;\n onError?: RequestReplyErrorHandler | undefined;\n logger: Logger;\n options?: Partial<RunExecutorOptions> | undefined;\n }) {\n const { heartbeatIntervalMs, replyTtlMs } = resolveRunExecutorOptions(options);\n this.executorId = executorId;\n this.redis = redis;\n this.subscriberClient = subscriberClient;\n this.logger = logger;\n this.replyTtlMs = replyTtlMs;\n this.requestHandler = requestHandler;\n this.onError = onError;\n this.channel = requestChannel(executorId);\n this.heartbeatKey = heartbeatKey(executorId);\n this.heartbeatIntervalMs = heartbeatIntervalMs;\n this.heartbeatTtlMs = Math.ceil(this.heartbeatIntervalMs * 1.5);\n }\n\n async init(): Promise<void> {\n if (this.closed || this.initialized) {\n return;\n }\n this.initialized = true;\n\n this.subscriberClient.on('error', (err: Error) => {\n this.logger.error('[RequestReplyExecutor] Subscriber error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n this.onError?.(err, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n });\n this.subscriberClient.on('end', () => {\n this.logger.warn('[RequestReplyExecutor] Subscriber connection ended', { executorId: this.executorId });\n this.stopHeartbeat();\n });\n // ready fires on the first connect and every reconnect; re-subscribing is\n // safe because node-redis dedupes the stable listener.\n this.subscriberClient.on('ready', () => {\n void this.setupSubscriber();\n });\n\n // On stable connections we won't get another ready event — if the caller\n // already connected the client, subscribe now.\n if (this.subscriberClient.isReady) {\n await this.setupSubscriber();\n }\n }\n\n private async setupSubscriber(): Promise<void> {\n if (this.closed) {\n return;\n }\n try {\n await this.subscriberClient.subscribe(this.channel, this.subscribeListener);\n this.logger.info('[RequestReplyExecutor] Subscribed to request channel', { executorId: this.executorId });\n this.stopHeartbeat();\n this.startHeartbeat();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err), { cause: err });\n this.logger.error('[RequestReplyExecutor] Error subscribing to request channel', {\n executorId: this.executorId,\n ...extractErrorLogFields(error),\n });\n this.onError?.(error, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n }\n }\n\n private async beat(): Promise<void> {\n this.logger.debug('[RequestReplyExecutor] Beating heartbeat', { executorId: this.executorId });\n try {\n await this.redis.set(this.heartbeatKey, '1', { PX: this.heartbeatTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Heartbeat error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** Heartbeat only while subscribed. Idempotent (restarts the timer). */\n private startHeartbeat(): void {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Starting heartbeat', { executorId: this.executorId });\n this.stopHeartbeat();\n void this.beat();\n this.heartbeatTimer = setInterval(() => {\n void this.beat();\n }, this.heartbeatIntervalMs);\n }\n\n /** A stopped heartbeat makes callers fail fast with NoResponderError. */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n this.logger.info('[RequestReplyExecutor] Stopping heartbeat', { executorId: this.executorId });\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private async processRequestFromMessage(message: string): Promise<void> {\n let raw: unknown;\n try {\n raw = JSON.parse(message);\n } catch (err) {\n // Drop the request, this should never happen\n this.logger.warn('[RequestReplyExecutor] Request message is not valid JSON', { executorId: this.executorId });\n this.onError?.(\n new Error(`Request message is not valid JSON for executorId: ${this.executorId}`, {\n cause: err,\n }),\n {\n executorId: this.executorId,\n channel: this.channel,\n },\n );\n return;\n }\n\n const parsedRequest = publishedRequestSchema.safeParse(raw);\n if (!parsedRequest.success) {\n // Drop the request, this should never happen\n const zodError = z.treeifyError(parsedRequest.error);\n this.logger.warn('[RequestReplyExecutor] Invalid request message shape', {\n executorId: this.executorId,\n zod: zodError,\n });\n this.onError?.(\n new Error(\n `Invalid request message shape for executorId: ${this.executorId}, error: ${JSON.stringify(zodError)}`,\n { cause: parsedRequest.error },\n ),\n { executorId: this.executorId, channel: this.channel },\n );\n return;\n }\n\n const { replyKey, path, body, headers } = parsedRequest.data;\n let replyPayload: JSONReply;\n try {\n replyPayload = await this.requestHandler(path, { body, headers });\n } catch (err) {\n // ReplyError lets handlers choose the reply status; any other throw is an unexpected 500.\n if (err instanceof ReplyError) {\n replyPayload = { status: err.status, headers: {}, body: { message: err.message } };\n } else {\n const message = err instanceof Error ? err.message : String(err);\n replyPayload = { status: 500, headers: {}, body: { message } };\n }\n }\n\n try {\n await this.redis.set(replyKey, JSON.stringify(replyPayload), { PX: this.replyTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Request processing failed', {\n executorId: this.executorId,\n replyKey,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** One raw pub/sub payload. Never throws. */\n private onMessage(message: string): void {\n this.logger.debug('[RequestReplyExecutor] Received message', { executorId: this.executorId, message });\n if (!message) {\n return;\n }\n if (this.closed) {\n this.logger.warn('[RequestReplyExecutor] Received message after executor is closed', {\n executorId: this.executorId,\n message,\n });\n return;\n }\n const pid = randomUUID();\n const requestPromise = this.processRequestFromMessage(message).finally(() => {\n this.runningRequests.delete(pid);\n });\n this.runningRequests.set(pid, requestPromise);\n }\n\n // This function should never throw!\n // Stops intake: unsubscribe from the channel and stop the heartbeat. Never\n // closes the clients — the caller owns their lifecycle.\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Closing', { executorId: this.executorId });\n this.closed = true;\n try {\n await this.subscriberClient.unsubscribe(this.channel);\n } catch {\n // ignore: connection may already be closed\n }\n this.stopHeartbeat();\n }\n\n // This function should never throw!\n // This is called during graceful shutdown\n async drain(): Promise<void> {\n await this.close();\n await Promise.allSettled(Array.from(this.runningRequests.values()));\n }\n}\n"],"mappings":"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","names":["z","message"]}
1
+ {"version":3,"sources":["../../src/request-reply/executor.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Logger } from 'winston';\nimport z from 'zod';\nimport { extractErrorLogFields } from '../core/util/errorLogFields';\nimport { ReplyError } from './errors';\nimport type { RedisClient } from './redisClient';\nimport type { JSONReply, RequestHandler } from './types';\nimport { publishedRequestSchema } from './types';\nimport { heartbeatKey, requestChannel } from './utils';\n\nexport interface RunExecutorOptions {\n /** How often to refresh the heartbeat key (ms). */\n heartbeatIntervalMs: number;\n /** TTL for the reply value so abandoned keys are reclaimed (ms). */\n replyTtlMs: number;\n}\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;\nconst DEFAULT_REPLY_TTL_MS = 120_000;\n\nfunction resolveRunExecutorOptions(options: Partial<RunExecutorOptions> | undefined): RunExecutorOptions {\n return {\n heartbeatIntervalMs: options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,\n replyTtlMs: options?.replyTtlMs ?? DEFAULT_REPLY_TTL_MS,\n };\n}\n\n/** Observability hook for subscriber errors, failed SUBSCRIBEs, and malformed messages. */\nexport type RequestReplyErrorHandler = (err: Error, context: { executorId: string; channel: string }) => void;\n\n/**\n * Serving side of the request-reply transport: parses and validates each\n * published message, dispatches it to the request handler, writes the\n * JSONReply to the request's reply key, and refreshes the heartbeat key while\n * subscribed.\n *\n * Connection lifecycle stays with the caller: both clients are supplied\n * connected and are never closed here. `subscriberClient` is the connection to\n * SUBSCRIBE on — a `duplicate()` for standalone Redis, or the shared client\n * for Sentinel — and must have a reconnect strategy so a failed connect\n * self-heals (the executor re-subscribes on every `ready`).\n */\nexport class RequestReplyExecutor {\n readonly executorId: string;\n /** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */\n readonly channel: string;\n private readonly redis: RedisClient;\n private readonly subscriberClient: RedisClient;\n private readonly logger: Logger;\n private readonly heartbeatIntervalMs: number;\n private readonly heartbeatTtlMs: number;\n private readonly replyTtlMs: number;\n private readonly heartbeatKey: string;\n private readonly requestHandler: RequestHandler;\n private readonly onError: RequestReplyErrorHandler | undefined;\n /** Stable reference so node-redis dedupes the listener across re-subscribes. */\n private readonly subscribeListener = (message: string): void => {\n this.onMessage(message);\n };\n\n private closed = false;\n private initialized = false;\n private runningRequests = new Map<string, Promise<void>>();\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor({\n executorId,\n redis,\n subscriberClient,\n requestHandler,\n onError,\n logger,\n options,\n }: {\n executorId: string;\n /** Connected command client, used only for SET (reply + heartbeat). Caller owns its lifecycle. */\n redis: RedisClient;\n /** Connected client to SUBSCRIBE on (duplicate or Sentinel). Caller owns its lifecycle. */\n subscriberClient: RedisClient;\n requestHandler: RequestHandler;\n onError?: RequestReplyErrorHandler | undefined;\n logger: Logger;\n options?: Partial<RunExecutorOptions> | undefined;\n }) {\n const { heartbeatIntervalMs, replyTtlMs } = resolveRunExecutorOptions(options);\n this.executorId = executorId;\n this.redis = redis;\n this.subscriberClient = subscriberClient;\n this.logger = logger;\n this.replyTtlMs = replyTtlMs;\n this.requestHandler = requestHandler;\n this.onError = onError;\n this.channel = requestChannel(executorId);\n this.heartbeatKey = heartbeatKey(executorId);\n this.heartbeatIntervalMs = heartbeatIntervalMs;\n this.heartbeatTtlMs = Math.ceil(this.heartbeatIntervalMs * 1.5);\n }\n\n async init(): Promise<void> {\n if (this.closed || this.initialized) {\n return;\n }\n this.initialized = true;\n\n this.subscriberClient.on('error', (err: Error) => {\n this.logger.error('[RequestReplyExecutor] Subscriber error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n this.onError?.(err, { executorId: this.executorId, channel: this.channel });\n // error is often transient; the client can reconnect and fire ready again. Do not stop the heartbeat.\n });\n this.subscriberClient.on('end', () => {\n this.logger.warn('[RequestReplyExecutor] Subscriber connection ended', { executorId: this.executorId });\n this.stopHeartbeat();\n });\n // ready fires on the first connect and every reconnect; re-subscribing is\n // safe because node-redis dedupes the stable listener.\n this.subscriberClient.on('ready', () => {\n void this.setupSubscriber();\n });\n\n // On stable connections we won't get another ready event — if the caller\n // already connected the client, subscribe now.\n if (this.subscriberClient.isReady) {\n await this.setupSubscriber();\n }\n }\n\n private async setupSubscriber(): Promise<void> {\n if (this.closed) {\n return;\n }\n try {\n await this.subscriberClient.subscribe(this.channel, this.subscribeListener);\n this.logger.info('[RequestReplyExecutor] Subscribed to request channel', { executorId: this.executorId });\n this.stopHeartbeat();\n this.startHeartbeat();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err), { cause: err });\n this.logger.error('[RequestReplyExecutor] Error subscribing to request channel', {\n executorId: this.executorId,\n ...extractErrorLogFields(error),\n });\n this.onError?.(error, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n }\n }\n\n private async beat(): Promise<void> {\n this.logger.debug('[RequestReplyExecutor] Beating heartbeat', { executorId: this.executorId });\n try {\n await this.redis.set(this.heartbeatKey, '1', { PX: this.heartbeatTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Heartbeat error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** Heartbeat only while subscribed. Idempotent (restarts the timer). */\n private startHeartbeat(): void {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Starting heartbeat', { executorId: this.executorId });\n this.stopHeartbeat();\n void this.beat();\n this.heartbeatTimer = setInterval(() => {\n void this.beat();\n }, this.heartbeatIntervalMs);\n }\n\n /** A stopped heartbeat makes callers fail fast with NoResponderError. */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n this.logger.info('[RequestReplyExecutor] Stopping heartbeat', { executorId: this.executorId });\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private async processRequestFromMessage(message: string): Promise<void> {\n let raw: unknown;\n try {\n raw = JSON.parse(message);\n } catch (err) {\n // Drop the request, this should never happen\n this.logger.warn('[RequestReplyExecutor] Request message is not valid JSON', { executorId: this.executorId });\n this.onError?.(\n new Error(`Request message is not valid JSON for executorId: ${this.executorId}`, {\n cause: err,\n }),\n {\n executorId: this.executorId,\n channel: this.channel,\n },\n );\n return;\n }\n\n const parsedRequest = publishedRequestSchema.safeParse(raw);\n if (!parsedRequest.success) {\n // Drop the request, this should never happen\n const zodError = z.treeifyError(parsedRequest.error);\n this.logger.warn('[RequestReplyExecutor] Invalid request message shape', {\n executorId: this.executorId,\n zod: zodError,\n });\n this.onError?.(\n new Error(\n `Invalid request message shape for executorId: ${this.executorId}, error: ${JSON.stringify(zodError)}`,\n { cause: parsedRequest.error },\n ),\n { executorId: this.executorId, channel: this.channel },\n );\n return;\n }\n\n const { replyKey, path, body, headers } = parsedRequest.data;\n let replyPayload: JSONReply;\n try {\n replyPayload = await this.requestHandler(path, { body, headers });\n } catch (err) {\n // ReplyError lets handlers choose the reply status; any other throw is an unexpected 500.\n if (err instanceof ReplyError) {\n replyPayload = { status: err.status, headers: {}, body: { message: err.message } };\n } else {\n const message = err instanceof Error ? err.message : String(err);\n replyPayload = { status: 500, headers: {}, body: { message } };\n }\n }\n\n try {\n await this.redis.set(replyKey, JSON.stringify(replyPayload), { PX: this.replyTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Request processing failed', {\n executorId: this.executorId,\n replyKey,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** One raw pub/sub payload. Never throws. */\n private onMessage(message: string): void {\n this.logger.debug('[RequestReplyExecutor] Received message', { executorId: this.executorId, message });\n if (!message) {\n return;\n }\n if (this.closed) {\n this.logger.warn('[RequestReplyExecutor] Received message after executor is closed', {\n executorId: this.executorId,\n message,\n });\n return;\n }\n const pid = randomUUID();\n const requestPromise = this.processRequestFromMessage(message).finally(() => {\n this.runningRequests.delete(pid);\n });\n this.runningRequests.set(pid, requestPromise);\n }\n\n // This function should never throw!\n // Stops intake: unsubscribe from the channel and stop the heartbeat. Never\n // closes the clients — the caller owns their lifecycle.\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Closing', { executorId: this.executorId });\n this.closed = true;\n try {\n await this.subscriberClient.unsubscribe(this.channel);\n } catch {\n // ignore: connection may already be closed\n }\n this.stopHeartbeat();\n }\n\n // This function should never throw!\n // This is called during graceful shutdown\n async drain(): Promise<void> {\n await this.close();\n await Promise.allSettled(Array.from(this.runningRequests.values()));\n }\n}\n"],"mappings":"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","names":["z","message"]}
@@ -67,7 +67,6 @@ var RequestReplyExecutor = class {
67
67
  ...extractErrorLogFields(err)
68
68
  });
69
69
  this.onError?.(err, { executorId: this.executorId, channel: this.channel });
70
- this.stopHeartbeat();
71
70
  });
72
71
  this.subscriberClient.on("end", () => {
73
72
  this.logger.warn("[RequestReplyExecutor] Subscriber connection ended", { executorId: this.executorId });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/request-reply/executor.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { RedisClientType } from 'redis';\nimport type { Logger } from 'winston';\nimport z from 'zod';\nimport { extractErrorLogFields } from '../core/util/errorLogFields';\nimport { ReplyError } from './errors';\nimport type { JSONReply, RequestHandler } from './types';\nimport { publishedRequestSchema } from './types';\nimport { heartbeatKey, requestChannel } from './utils';\n\nexport interface RunExecutorOptions {\n /** How often to refresh the heartbeat key (ms). */\n heartbeatIntervalMs: number;\n /** TTL for the reply value so abandoned keys are reclaimed (ms). */\n replyTtlMs: number;\n}\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;\nconst DEFAULT_REPLY_TTL_MS = 120_000;\n\nfunction resolveRunExecutorOptions(options: Partial<RunExecutorOptions> | undefined): RunExecutorOptions {\n return {\n heartbeatIntervalMs: options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,\n replyTtlMs: options?.replyTtlMs ?? DEFAULT_REPLY_TTL_MS,\n };\n}\n\n/** Observability hook for subscriber errors, failed SUBSCRIBEs, and malformed messages. */\nexport type RequestReplyErrorHandler = (err: Error, context: { executorId: string; channel: string }) => void;\n\n/**\n * Serving side of the request-reply transport: parses and validates each\n * published message, dispatches it to the request handler, writes the\n * JSONReply to the request's reply key, and refreshes the heartbeat key while\n * subscribed.\n *\n * Connection lifecycle stays with the caller: both clients are supplied\n * connected and are never closed here. `subscriberClient` is the connection to\n * SUBSCRIBE on — a `duplicate()` for standalone Redis, or the shared client\n * for Sentinel — and must have a reconnect strategy so a failed connect\n * self-heals (the executor re-subscribes on every `ready`).\n */\nexport class RequestReplyExecutor {\n readonly executorId: string;\n /** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */\n readonly channel: string;\n private readonly redis: RedisClientType;\n private readonly subscriberClient: RedisClientType;\n private readonly logger: Logger;\n private readonly heartbeatIntervalMs: number;\n private readonly heartbeatTtlMs: number;\n private readonly replyTtlMs: number;\n private readonly heartbeatKey: string;\n private readonly requestHandler: RequestHandler;\n private readonly onError: RequestReplyErrorHandler | undefined;\n /** Stable reference so node-redis dedupes the listener across re-subscribes. */\n private readonly subscribeListener = (message: string): void => {\n this.onMessage(message);\n };\n\n private closed = false;\n private initialized = false;\n private runningRequests = new Map<string, Promise<void>>();\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor({\n executorId,\n redis,\n subscriberClient,\n requestHandler,\n onError,\n logger,\n options,\n }: {\n executorId: string;\n /** Connected command client, used only for SET (reply + heartbeat). Caller owns its lifecycle. */\n redis: RedisClientType;\n /** Connected client to SUBSCRIBE on (duplicate or Sentinel). Caller owns its lifecycle. */\n subscriberClient: RedisClientType;\n requestHandler: RequestHandler;\n onError?: RequestReplyErrorHandler | undefined;\n logger: Logger;\n options?: Partial<RunExecutorOptions> | undefined;\n }) {\n const { heartbeatIntervalMs, replyTtlMs } = resolveRunExecutorOptions(options);\n this.executorId = executorId;\n this.redis = redis;\n this.subscriberClient = subscriberClient;\n this.logger = logger;\n this.replyTtlMs = replyTtlMs;\n this.requestHandler = requestHandler;\n this.onError = onError;\n this.channel = requestChannel(executorId);\n this.heartbeatKey = heartbeatKey(executorId);\n this.heartbeatIntervalMs = heartbeatIntervalMs;\n this.heartbeatTtlMs = Math.ceil(this.heartbeatIntervalMs * 1.5);\n }\n\n async init(): Promise<void> {\n if (this.closed || this.initialized) {\n return;\n }\n this.initialized = true;\n\n this.subscriberClient.on('error', (err: Error) => {\n this.logger.error('[RequestReplyExecutor] Subscriber error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n this.onError?.(err, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n });\n this.subscriberClient.on('end', () => {\n this.logger.warn('[RequestReplyExecutor] Subscriber connection ended', { executorId: this.executorId });\n this.stopHeartbeat();\n });\n // ready fires on the first connect and every reconnect; re-subscribing is\n // safe because node-redis dedupes the stable listener.\n this.subscriberClient.on('ready', () => {\n void this.setupSubscriber();\n });\n\n // On stable connections we won't get another ready event — if the caller\n // already connected the client, subscribe now.\n if (this.subscriberClient.isReady) {\n await this.setupSubscriber();\n }\n }\n\n private async setupSubscriber(): Promise<void> {\n if (this.closed) {\n return;\n }\n try {\n await this.subscriberClient.subscribe(this.channel, this.subscribeListener);\n this.logger.info('[RequestReplyExecutor] Subscribed to request channel', { executorId: this.executorId });\n this.stopHeartbeat();\n this.startHeartbeat();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err), { cause: err });\n this.logger.error('[RequestReplyExecutor] Error subscribing to request channel', {\n executorId: this.executorId,\n ...extractErrorLogFields(error),\n });\n this.onError?.(error, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n }\n }\n\n private async beat(): Promise<void> {\n this.logger.debug('[RequestReplyExecutor] Beating heartbeat', { executorId: this.executorId });\n try {\n await this.redis.set(this.heartbeatKey, '1', { PX: this.heartbeatTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Heartbeat error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** Heartbeat only while subscribed. Idempotent (restarts the timer). */\n private startHeartbeat(): void {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Starting heartbeat', { executorId: this.executorId });\n this.stopHeartbeat();\n void this.beat();\n this.heartbeatTimer = setInterval(() => {\n void this.beat();\n }, this.heartbeatIntervalMs);\n }\n\n /** A stopped heartbeat makes callers fail fast with NoResponderError. */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n this.logger.info('[RequestReplyExecutor] Stopping heartbeat', { executorId: this.executorId });\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private async processRequestFromMessage(message: string): Promise<void> {\n let raw: unknown;\n try {\n raw = JSON.parse(message);\n } catch (err) {\n // Drop the request, this should never happen\n this.logger.warn('[RequestReplyExecutor] Request message is not valid JSON', { executorId: this.executorId });\n this.onError?.(\n new Error(`Request message is not valid JSON for executorId: ${this.executorId}`, {\n cause: err,\n }),\n {\n executorId: this.executorId,\n channel: this.channel,\n },\n );\n return;\n }\n\n const parsedRequest = publishedRequestSchema.safeParse(raw);\n if (!parsedRequest.success) {\n // Drop the request, this should never happen\n const zodError = z.treeifyError(parsedRequest.error);\n this.logger.warn('[RequestReplyExecutor] Invalid request message shape', {\n executorId: this.executorId,\n zod: zodError,\n });\n this.onError?.(\n new Error(\n `Invalid request message shape for executorId: ${this.executorId}, error: ${JSON.stringify(zodError)}`,\n { cause: parsedRequest.error },\n ),\n { executorId: this.executorId, channel: this.channel },\n );\n return;\n }\n\n const { replyKey, path, body, headers } = parsedRequest.data;\n let replyPayload: JSONReply;\n try {\n replyPayload = await this.requestHandler(path, { body, headers });\n } catch (err) {\n // ReplyError lets handlers choose the reply status; any other throw is an unexpected 500.\n if (err instanceof ReplyError) {\n replyPayload = { status: err.status, headers: {}, body: { message: err.message } };\n } else {\n const message = err instanceof Error ? err.message : String(err);\n replyPayload = { status: 500, headers: {}, body: { message } };\n }\n }\n\n try {\n await this.redis.set(replyKey, JSON.stringify(replyPayload), { PX: this.replyTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Request processing failed', {\n executorId: this.executorId,\n replyKey,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** One raw pub/sub payload. Never throws. */\n private onMessage(message: string): void {\n this.logger.debug('[RequestReplyExecutor] Received message', { executorId: this.executorId, message });\n if (!message) {\n return;\n }\n if (this.closed) {\n this.logger.warn('[RequestReplyExecutor] Received message after executor is closed', {\n executorId: this.executorId,\n message,\n });\n return;\n }\n const pid = randomUUID();\n const requestPromise = this.processRequestFromMessage(message).finally(() => {\n this.runningRequests.delete(pid);\n });\n this.runningRequests.set(pid, requestPromise);\n }\n\n // This function should never throw!\n // Stops intake: unsubscribe from the channel and stop the heartbeat. Never\n // closes the clients — the caller owns their lifecycle.\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Closing', { executorId: this.executorId });\n this.closed = true;\n try {\n await this.subscriberClient.unsubscribe(this.channel);\n } catch {\n // ignore: connection may already be closed\n }\n this.stopHeartbeat();\n }\n\n // This function should never throw!\n // This is called during graceful shutdown\n async drain(): Promise<void> {\n await this.close();\n await Promise.allSettled(Array.from(this.runningRequests.values()));\n }\n}\n"],"mappings":"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","names":["message"]}
1
+ {"version":3,"sources":["../../src/request-reply/executor.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Logger } from 'winston';\nimport z from 'zod';\nimport { extractErrorLogFields } from '../core/util/errorLogFields';\nimport { ReplyError } from './errors';\nimport type { RedisClient } from './redisClient';\nimport type { JSONReply, RequestHandler } from './types';\nimport { publishedRequestSchema } from './types';\nimport { heartbeatKey, requestChannel } from './utils';\n\nexport interface RunExecutorOptions {\n /** How often to refresh the heartbeat key (ms). */\n heartbeatIntervalMs: number;\n /** TTL for the reply value so abandoned keys are reclaimed (ms). */\n replyTtlMs: number;\n}\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;\nconst DEFAULT_REPLY_TTL_MS = 120_000;\n\nfunction resolveRunExecutorOptions(options: Partial<RunExecutorOptions> | undefined): RunExecutorOptions {\n return {\n heartbeatIntervalMs: options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,\n replyTtlMs: options?.replyTtlMs ?? DEFAULT_REPLY_TTL_MS,\n };\n}\n\n/** Observability hook for subscriber errors, failed SUBSCRIBEs, and malformed messages. */\nexport type RequestReplyErrorHandler = (err: Error, context: { executorId: string; channel: string }) => void;\n\n/**\n * Serving side of the request-reply transport: parses and validates each\n * published message, dispatches it to the request handler, writes the\n * JSONReply to the request's reply key, and refreshes the heartbeat key while\n * subscribed.\n *\n * Connection lifecycle stays with the caller: both clients are supplied\n * connected and are never closed here. `subscriberClient` is the connection to\n * SUBSCRIBE on — a `duplicate()` for standalone Redis, or the shared client\n * for Sentinel — and must have a reconnect strategy so a failed connect\n * self-heals (the executor re-subscribes on every `ready`).\n */\nexport class RequestReplyExecutor {\n readonly executorId: string;\n /** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */\n readonly channel: string;\n private readonly redis: RedisClient;\n private readonly subscriberClient: RedisClient;\n private readonly logger: Logger;\n private readonly heartbeatIntervalMs: number;\n private readonly heartbeatTtlMs: number;\n private readonly replyTtlMs: number;\n private readonly heartbeatKey: string;\n private readonly requestHandler: RequestHandler;\n private readonly onError: RequestReplyErrorHandler | undefined;\n /** Stable reference so node-redis dedupes the listener across re-subscribes. */\n private readonly subscribeListener = (message: string): void => {\n this.onMessage(message);\n };\n\n private closed = false;\n private initialized = false;\n private runningRequests = new Map<string, Promise<void>>();\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor({\n executorId,\n redis,\n subscriberClient,\n requestHandler,\n onError,\n logger,\n options,\n }: {\n executorId: string;\n /** Connected command client, used only for SET (reply + heartbeat). Caller owns its lifecycle. */\n redis: RedisClient;\n /** Connected client to SUBSCRIBE on (duplicate or Sentinel). Caller owns its lifecycle. */\n subscriberClient: RedisClient;\n requestHandler: RequestHandler;\n onError?: RequestReplyErrorHandler | undefined;\n logger: Logger;\n options?: Partial<RunExecutorOptions> | undefined;\n }) {\n const { heartbeatIntervalMs, replyTtlMs } = resolveRunExecutorOptions(options);\n this.executorId = executorId;\n this.redis = redis;\n this.subscriberClient = subscriberClient;\n this.logger = logger;\n this.replyTtlMs = replyTtlMs;\n this.requestHandler = requestHandler;\n this.onError = onError;\n this.channel = requestChannel(executorId);\n this.heartbeatKey = heartbeatKey(executorId);\n this.heartbeatIntervalMs = heartbeatIntervalMs;\n this.heartbeatTtlMs = Math.ceil(this.heartbeatIntervalMs * 1.5);\n }\n\n async init(): Promise<void> {\n if (this.closed || this.initialized) {\n return;\n }\n this.initialized = true;\n\n this.subscriberClient.on('error', (err: Error) => {\n this.logger.error('[RequestReplyExecutor] Subscriber error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n this.onError?.(err, { executorId: this.executorId, channel: this.channel });\n // error is often transient; the client can reconnect and fire ready again. Do not stop the heartbeat.\n });\n this.subscriberClient.on('end', () => {\n this.logger.warn('[RequestReplyExecutor] Subscriber connection ended', { executorId: this.executorId });\n this.stopHeartbeat();\n });\n // ready fires on the first connect and every reconnect; re-subscribing is\n // safe because node-redis dedupes the stable listener.\n this.subscriberClient.on('ready', () => {\n void this.setupSubscriber();\n });\n\n // On stable connections we won't get another ready event — if the caller\n // already connected the client, subscribe now.\n if (this.subscriberClient.isReady) {\n await this.setupSubscriber();\n }\n }\n\n private async setupSubscriber(): Promise<void> {\n if (this.closed) {\n return;\n }\n try {\n await this.subscriberClient.subscribe(this.channel, this.subscribeListener);\n this.logger.info('[RequestReplyExecutor] Subscribed to request channel', { executorId: this.executorId });\n this.stopHeartbeat();\n this.startHeartbeat();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err), { cause: err });\n this.logger.error('[RequestReplyExecutor] Error subscribing to request channel', {\n executorId: this.executorId,\n ...extractErrorLogFields(error),\n });\n this.onError?.(error, { executorId: this.executorId, channel: this.channel });\n this.stopHeartbeat();\n }\n }\n\n private async beat(): Promise<void> {\n this.logger.debug('[RequestReplyExecutor] Beating heartbeat', { executorId: this.executorId });\n try {\n await this.redis.set(this.heartbeatKey, '1', { PX: this.heartbeatTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Heartbeat error', {\n executorId: this.executorId,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** Heartbeat only while subscribed. Idempotent (restarts the timer). */\n private startHeartbeat(): void {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Starting heartbeat', { executorId: this.executorId });\n this.stopHeartbeat();\n void this.beat();\n this.heartbeatTimer = setInterval(() => {\n void this.beat();\n }, this.heartbeatIntervalMs);\n }\n\n /** A stopped heartbeat makes callers fail fast with NoResponderError. */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n this.logger.info('[RequestReplyExecutor] Stopping heartbeat', { executorId: this.executorId });\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private async processRequestFromMessage(message: string): Promise<void> {\n let raw: unknown;\n try {\n raw = JSON.parse(message);\n } catch (err) {\n // Drop the request, this should never happen\n this.logger.warn('[RequestReplyExecutor] Request message is not valid JSON', { executorId: this.executorId });\n this.onError?.(\n new Error(`Request message is not valid JSON for executorId: ${this.executorId}`, {\n cause: err,\n }),\n {\n executorId: this.executorId,\n channel: this.channel,\n },\n );\n return;\n }\n\n const parsedRequest = publishedRequestSchema.safeParse(raw);\n if (!parsedRequest.success) {\n // Drop the request, this should never happen\n const zodError = z.treeifyError(parsedRequest.error);\n this.logger.warn('[RequestReplyExecutor] Invalid request message shape', {\n executorId: this.executorId,\n zod: zodError,\n });\n this.onError?.(\n new Error(\n `Invalid request message shape for executorId: ${this.executorId}, error: ${JSON.stringify(zodError)}`,\n { cause: parsedRequest.error },\n ),\n { executorId: this.executorId, channel: this.channel },\n );\n return;\n }\n\n const { replyKey, path, body, headers } = parsedRequest.data;\n let replyPayload: JSONReply;\n try {\n replyPayload = await this.requestHandler(path, { body, headers });\n } catch (err) {\n // ReplyError lets handlers choose the reply status; any other throw is an unexpected 500.\n if (err instanceof ReplyError) {\n replyPayload = { status: err.status, headers: {}, body: { message: err.message } };\n } else {\n const message = err instanceof Error ? err.message : String(err);\n replyPayload = { status: 500, headers: {}, body: { message } };\n }\n }\n\n try {\n await this.redis.set(replyKey, JSON.stringify(replyPayload), { PX: this.replyTtlMs });\n } catch (err) {\n this.logger.error('[RequestReplyExecutor] Request processing failed', {\n executorId: this.executorId,\n replyKey,\n ...extractErrorLogFields(err),\n });\n }\n }\n\n /** One raw pub/sub payload. Never throws. */\n private onMessage(message: string): void {\n this.logger.debug('[RequestReplyExecutor] Received message', { executorId: this.executorId, message });\n if (!message) {\n return;\n }\n if (this.closed) {\n this.logger.warn('[RequestReplyExecutor] Received message after executor is closed', {\n executorId: this.executorId,\n message,\n });\n return;\n }\n const pid = randomUUID();\n const requestPromise = this.processRequestFromMessage(message).finally(() => {\n this.runningRequests.delete(pid);\n });\n this.runningRequests.set(pid, requestPromise);\n }\n\n // This function should never throw!\n // Stops intake: unsubscribe from the channel and stop the heartbeat. Never\n // closes the clients — the caller owns their lifecycle.\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.logger.info('[RequestReplyExecutor] Closing', { executorId: this.executorId });\n this.closed = true;\n try {\n await this.subscriberClient.unsubscribe(this.channel);\n } catch {\n // ignore: connection may already be closed\n }\n this.stopHeartbeat();\n }\n\n // This function should never throw!\n // This is called during graceful shutdown\n async drain(): Promise<void> {\n await this.close();\n await Promise.allSettled(Array.from(this.runningRequests.values()));\n }\n}\n"],"mappings":"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","names":["message"]}
@@ -9,6 +9,7 @@ export type { SendRequestOptions } from './client';
9
9
  export { NoResponderError, ReplyError, RequestTimeoutError } from './errors';
10
10
  export { RequestReplyExecutor } from './executor';
11
11
  export type { RequestReplyErrorHandler, RunExecutorOptions } from './executor';
12
+ export type { RedisClient } from './redisClient';
12
13
  export { RequestReplyRouter } from './router';
13
14
  export type { RouteHandler } from './router';
14
15
  export { jsonReplySchema } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/request-reply/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/request-reply/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/request-reply/index.ts"],"sourcesContent":["/**\n * Redis request-reply transport for executor peering: route a command to the\n * replica addressed by a plain `executorId`. Hosts own the Redis connection\n * (inject the node-redis client) and their own id grammar — how an executor\n * id is embedded in / parsed out of resource ids never enters this module.\n */\n\nexport { redisRequest } from './client';\nexport type { SendRequestOptions } from './client';\nexport { NoResponderError, ReplyError, RequestTimeoutError } from './errors';\nexport { RequestReplyExecutor } from './executor';\nexport type { RequestReplyErrorHandler, RunExecutorOptions } from './executor';\nexport { RequestReplyRouter } from './router';\nexport type { RouteHandler } from './router';\nexport { jsonReplySchema } from './types';\nexport type { JSONReply, JSONValue, RequestEnvelope, RequestHandler } from './types';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,oBAA6B;AAE7B,oBAAkE;AAClE,sBAAqC;AAErC,oBAAmC;AAEnC,mBAAgC;","names":[]}
1
+ {"version":3,"sources":["../../src/request-reply/index.ts"],"sourcesContent":["/**\n * Redis request-reply transport for executor peering: route a command to the\n * replica addressed by a plain `executorId`. Hosts own the Redis connection\n * (inject the node-redis client) and their own id grammar — how an executor\n * id is embedded in / parsed out of resource ids never enters this module.\n */\n\nexport { redisRequest } from './client';\nexport type { SendRequestOptions } from './client';\nexport { NoResponderError, ReplyError, RequestTimeoutError } from './errors';\nexport { RequestReplyExecutor } from './executor';\nexport type { RequestReplyErrorHandler, RunExecutorOptions } from './executor';\nexport type { RedisClient } from './redisClient';\nexport { RequestReplyRouter } from './router';\nexport type { RouteHandler } from './router';\nexport { jsonReplySchema } from './types';\nexport type { JSONReply, JSONValue, RequestEnvelope, RequestHandler } from './types';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,oBAA6B;AAE7B,oBAAkE;AAClE,sBAAqC;AAGrC,oBAAmC;AAEnC,mBAAgC;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/request-reply/index.ts"],"sourcesContent":["/**\n * Redis request-reply transport for executor peering: route a command to the\n * replica addressed by a plain `executorId`. Hosts own the Redis connection\n * (inject the node-redis client) and their own id grammar — how an executor\n * id is embedded in / parsed out of resource ids never enters this module.\n */\n\nexport { redisRequest } from './client';\nexport type { SendRequestOptions } from './client';\nexport { NoResponderError, ReplyError, RequestTimeoutError } from './errors';\nexport { RequestReplyExecutor } from './executor';\nexport type { RequestReplyErrorHandler, RunExecutorOptions } from './executor';\nexport { RequestReplyRouter } from './router';\nexport type { RouteHandler } from './router';\nexport { jsonReplySchema } from './types';\nexport type { JSONReply, JSONValue, RequestEnvelope, RequestHandler } from './types';\n"],"mappings":";AAOA,SAAS,oBAAoB;AAE7B,SAAS,kBAAkB,YAAY,2BAA2B;AAClE,SAAS,4BAA4B;AAErC,SAAS,0BAA0B;AAEnC,SAAS,uBAAuB;","names":[]}
1
+ {"version":3,"sources":["../../src/request-reply/index.ts"],"sourcesContent":["/**\n * Redis request-reply transport for executor peering: route a command to the\n * replica addressed by a plain `executorId`. Hosts own the Redis connection\n * (inject the node-redis client) and their own id grammar — how an executor\n * id is embedded in / parsed out of resource ids never enters this module.\n */\n\nexport { redisRequest } from './client';\nexport type { SendRequestOptions } from './client';\nexport { NoResponderError, ReplyError, RequestTimeoutError } from './errors';\nexport { RequestReplyExecutor } from './executor';\nexport type { RequestReplyErrorHandler, RunExecutorOptions } from './executor';\nexport type { RedisClient } from './redisClient';\nexport { RequestReplyRouter } from './router';\nexport type { RouteHandler } from './router';\nexport { jsonReplySchema } from './types';\nexport type { JSONReply, JSONValue, RequestEnvelope, RequestHandler } from './types';\n"],"mappings":";AAOA,SAAS,oBAAoB;AAE7B,SAAS,kBAAkB,YAAY,2BAA2B;AAClE,SAAS,4BAA4B;AAGrC,SAAS,0BAA0B;AAEnC,SAAS,uBAAuB;","names":[]}
@@ -0,0 +1,4 @@
1
+ import type { RedisClientType, RedisSentinelType } from 'redis';
2
+ /** Standalone or Sentinel client for request-reply / command use. */
3
+ export type RedisClient = RedisClientType | RedisSentinelType;
4
+ //# sourceMappingURL=redisClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redisClient.d.ts","sourceRoot":"","sources":["../../src/request-reply/redisClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAEhE,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,iBAAiB,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/request-reply/redisClient.ts
17
+ var redisClient_exports = {};
18
+ module.exports = __toCommonJS(redisClient_exports);
19
+ //# sourceMappingURL=redisClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/request-reply/redisClient.ts"],"sourcesContent":["import type { RedisClientType, RedisSentinelType } from 'redis';\n\n/** Standalone or Sentinel client for request-reply / command use. */\nexport type RedisClient = RedisClientType | RedisSentinelType;\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=redisClient.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/trueforge-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-rc.0",
4
4
  "description": "TrueForge Agent Harness library.",
5
5
  "author": "TrueFoundry",
6
6
  "license": "MIT",