@truefoundry/trueforge-core 0.2.0-rc.2 → 0.2.0-rc.3
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/agent-session/TurnResourceResolver.d.ts +13 -10
- package/dist/agent-session/TurnResourceResolver.d.ts.map +1 -1
- package/dist/agent-session/TurnResourceResolver.js +15 -1
- package/dist/agent-session/TurnResourceResolver.js.map +1 -1
- package/dist/agent-session/TurnResourceResolver.mjs +15 -1
- package/dist/agent-session/TurnResourceResolver.mjs.map +1 -1
- package/dist/agent-session/index.d.ts +1 -1
- package/dist/agent-session/index.d.ts.map +1 -1
- package/dist/agent-session/index.js.map +1 -1
- package/dist/agent-session/index.mjs.map +1 -1
- package/dist/agent-session/schemas/agentSpec.d.ts +4 -2
- package/dist/agent-session/schemas/agentSpec.d.ts.map +1 -1
- package/dist/agent-session/schemas/agentSpec.js +5 -3
- package/dist/agent-session/schemas/agentSpec.js.map +1 -1
- package/dist/agent-session/schemas/agentSpec.mjs +5 -3
- package/dist/agent-session/schemas/agentSpec.mjs.map +1 -1
- package/dist/agent-session/schemas/session.d.ts +3 -0
- package/dist/agent-session/schemas/session.d.ts.map +1 -1
- package/dist/agent-session/store/ISessionStore.d.ts +10 -0
- package/dist/agent-session/store/ISessionStore.d.ts.map +1 -1
- package/dist/agent-session/store/ISessionStore.js.map +1 -1
- package/dist/agent-session/store/InMemorySessionStore.d.ts +2 -1
- package/dist/agent-session/store/InMemorySessionStore.d.ts.map +1 -1
- package/dist/agent-session/store/InMemorySessionStore.js +13 -0
- package/dist/agent-session/store/InMemorySessionStore.js.map +1 -1
- package/dist/agent-session/store/InMemorySessionStore.mjs +13 -0
- package/dist/agent-session/store/InMemorySessionStore.mjs.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +3 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/llm/VercelAILLM.d.ts.map +1 -1
- package/dist/core/llm/VercelAILLM.js +4 -0
- package/dist/core/llm/VercelAILLM.js.map +1 -1
- package/dist/core/llm/VercelAILLM.mjs +4 -0
- package/dist/core/llm/VercelAILLM.mjs.map +1 -1
- package/dist/core/redisKeys.d.ts +7 -0
- package/dist/core/redisKeys.d.ts.map +1 -0
- package/dist/core/redisKeys.js +36 -0
- package/dist/core/redisKeys.js.map +1 -0
- package/dist/core/redisKeys.mjs +10 -0
- package/dist/core/redisKeys.mjs.map +1 -0
- package/dist/core/sandbox/Sandbox.d.ts.map +1 -1
- package/dist/core/sandbox/Sandbox.js +8 -1
- package/dist/core/sandbox/Sandbox.js.map +1 -1
- package/dist/core/sandbox/Sandbox.mjs +8 -1
- package/dist/core/sandbox/Sandbox.mjs.map +1 -1
- package/dist/core/sandbox/provider/DaytonaProvider.d.ts +1 -1
- package/dist/core/sandbox/provider/DaytonaProvider.d.ts.map +1 -1
- package/dist/core/sandbox/provider/DaytonaProvider.js +3 -3
- package/dist/core/sandbox/provider/DaytonaProvider.js.map +1 -1
- package/dist/core/sandbox/provider/DaytonaProvider.mjs +3 -3
- package/dist/core/sandbox/provider/DaytonaProvider.mjs.map +1 -1
- package/dist/core/sandbox/provider/Provider.d.ts +20 -4
- package/dist/core/sandbox/provider/Provider.d.ts.map +1 -1
- package/dist/core/sandbox/provider/Provider.js.map +1 -1
- package/dist/core/sandbox/provider/Provider.mjs.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts +2 -2
- package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.js +4 -4
- package/dist/core/sandbox/provider/TFYSandboxProvider.js.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs +4 -4
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs.map +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.d.ts +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.d.ts.map +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.js +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.js.map +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.mjs +1 -1
- package/dist/core/sandbox/sandboxScripts.gen.mjs.map +1 -1
- package/dist/core/sandbox/skills/ISkillMounter.d.ts +1 -1
- package/dist/core/sandbox/skills/ISkillMounter.d.ts.map +1 -1
- package/dist/core/sandbox/skills/ISkillMounter.js.map +1 -1
- package/dist/core/sandbox/skills/SkillMounter.d.ts +20 -2
- package/dist/core/sandbox/skills/SkillMounter.d.ts.map +1 -1
- package/dist/core/sandbox/skills/SkillMounter.js +59 -23
- package/dist/core/sandbox/skills/SkillMounter.js.map +1 -1
- package/dist/core/sandbox/skills/SkillMounter.mjs +58 -23
- package/dist/core/sandbox/skills/SkillMounter.mjs.map +1 -1
- package/dist/core/sandbox/skills/index.d.ts +1 -1
- package/dist/core/sandbox/skills/index.d.ts.map +1 -1
- package/dist/core/sandbox/skills/index.js.map +1 -1
- package/dist/core/sandbox/skills/index.mjs.map +1 -1
- package/dist/request-reply/executor.d.ts +1 -1
- package/dist/request-reply/executor.d.ts.map +1 -1
- package/dist/request-reply/executor.js +1 -1
- package/dist/request-reply/executor.js.map +1 -1
- package/dist/request-reply/executor.mjs +1 -1
- package/dist/request-reply/executor.mjs.map +1 -1
- package/dist/request-reply/utils.d.ts +0 -1
- package/dist/request-reply/utils.d.ts.map +1 -1
- package/dist/request-reply/utils.js +4 -6
- package/dist/request-reply/utils.js.map +1 -1
- package/dist/request-reply/utils.mjs +4 -5
- package/dist/request-reply/utils.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/core/sandbox/skills/index.ts"],"sourcesContent":["export { SKILLS_PREAMBLE, getSkillPath, renderSkillPromptBody } from './constants';\nexport type { ISkillMounter } from './ISkillMounter';\nexport { SkillMounter } from './SkillMounter';\nexport type { GitSkill } from './SkillMounter';\n"],"mappings":";AAAA,SAAS,iBAAiB,cAAc,6BAA6B;AAErE,SAAS,oBAAoB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/core/sandbox/skills/index.ts"],"sourcesContent":["export { SKILLS_PREAMBLE, getSkillPath, renderSkillPromptBody } from './constants';\nexport type { ISkillMounter } from './ISkillMounter';\nexport { SkillMounter } from './SkillMounter';\nexport type { GitSkill, Skill } from './SkillMounter';\n"],"mappings":";AAAA,SAAS,iBAAiB,cAAc,6BAA6B;AAErE,SAAS,oBAAoB;","names":[]}
|
|
@@ -26,7 +26,7 @@ export type RequestReplyErrorHandler = (err: Error, context: {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare class RequestReplyExecutor {
|
|
28
28
|
readonly executorId: string;
|
|
29
|
-
/** `rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
29
|
+
/** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
30
30
|
readonly channel: string;
|
|
31
31
|
private readonly redis;
|
|
32
32
|
private readonly subscriberClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/request-reply/executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAItC,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,SAAS,CAAC;AAIzD,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;CACpB;AAYD,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAC;AAE9G;;;;;;;;;;;GAWG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/request-reply/executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAItC,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,SAAS,CAAC;AAIzD,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;CACpB;AAYD,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAC;AAE9G;;;;;;;;;;;GAWG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAEhC;IAEF,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAoC;IAC3D,OAAO,CAAC,cAAc,CAA+C;IAErE,YAAY,EACV,UAAU,EACV,KAAK,EACL,gBAAgB,EAChB,cAAc,EACd,OAAO,EACP,MAAM,EACN,OAAO,GACR,EAAE;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,kGAAkG;QAClG,KAAK,EAAE,eAAe,CAAC;QACvB,2FAA2F;QAC3F,gBAAgB,EAAE,eAAe,CAAC;QAClC,cAAc,EAAE,cAAc,CAAC;QAC/B,OAAO,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;KACnD,EAaA;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA6B1B;YAEa,eAAe;YAoBf,IAAI;IAYlB,wEAAwE;IACxE,OAAO,CAAC,cAAc;IAYtB,yEAAyE;IACzE,OAAO,CAAC,aAAa;YAQP,yBAAyB;IA8DvC,6CAA6C;IAC7C,OAAO,CAAC,SAAS;IAsBX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAY3B;IAIK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAG3B;CACF"}
|
|
@@ -49,7 +49,7 @@ function resolveRunExecutorOptions(options) {
|
|
|
49
49
|
}
|
|
50
50
|
var RequestReplyExecutor = class {
|
|
51
51
|
executorId;
|
|
52
|
-
/** `rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
52
|
+
/** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
53
53
|
channel;
|
|
54
54
|
redis;
|
|
55
55
|
subscriberClient;
|
|
@@ -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 /** `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 { 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"]}
|
|
@@ -15,7 +15,7 @@ function resolveRunExecutorOptions(options) {
|
|
|
15
15
|
}
|
|
16
16
|
var RequestReplyExecutor = class {
|
|
17
17
|
executorId;
|
|
18
|
-
/** `rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
18
|
+
/** `tfg:rr:req:<executorId>` — the channel this executor subscribes to. */
|
|
19
19
|
channel;
|
|
20
20
|
redis;
|
|
21
21
|
subscriberClient;
|
|
@@ -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 /** `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 { 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,5 +1,4 @@
|
|
|
1
1
|
export declare function sleep(ms: number): Promise<void>;
|
|
2
|
-
export declare const KEY_PREFIX = "rr";
|
|
3
2
|
/** Liveness key an executor refreshes; callers check it before publishing. */
|
|
4
3
|
export declare function heartbeatKey(executorId: string): string;
|
|
5
4
|
/** Pub/sub channel: executors subscribe; callers publish the JSON request (see publishedRequestSchema). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/request-reply/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/request-reply/utils.ts"],"names":[],"mappings":"AAEA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,2GAA2G;AAC3G,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,2FAA2F;AAC3F,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -20,29 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/request-reply/utils.ts
|
|
21
21
|
var utils_exports = {};
|
|
22
22
|
__export(utils_exports, {
|
|
23
|
-
KEY_PREFIX: () => KEY_PREFIX,
|
|
24
23
|
heartbeatKey: () => heartbeatKey,
|
|
25
24
|
replyKey: () => replyKey,
|
|
26
25
|
requestChannel: () => requestChannel,
|
|
27
26
|
sleep: () => sleep
|
|
28
27
|
});
|
|
29
28
|
module.exports = __toCommonJS(utils_exports);
|
|
29
|
+
var import_redisKeys = require("../core/redisKeys.js");
|
|
30
30
|
function sleep(ms) {
|
|
31
31
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
32
|
}
|
|
33
|
-
var KEY_PREFIX = "rr";
|
|
34
33
|
function heartbeatKey(executorId) {
|
|
35
|
-
return
|
|
34
|
+
return (0, import_redisKeys.redisKey)("rr", "hb", executorId);
|
|
36
35
|
}
|
|
37
36
|
function requestChannel(executorId) {
|
|
38
|
-
return
|
|
37
|
+
return (0, import_redisKeys.redisKey)("rr", "req", executorId);
|
|
39
38
|
}
|
|
40
39
|
function replyKey(requestId) {
|
|
41
|
-
return
|
|
40
|
+
return (0, import_redisKeys.redisKey)("rr", "reply", requestId);
|
|
42
41
|
}
|
|
43
42
|
// Annotate the CommonJS export names for ESM import in node:
|
|
44
43
|
0 && (module.exports = {
|
|
45
|
-
KEY_PREFIX,
|
|
46
44
|
heartbeatKey,
|
|
47
45
|
replyKey,
|
|
48
46
|
requestChannel,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/request-reply/utils.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/request-reply/utils.ts"],"sourcesContent":["import { redisKey } from '../core/redisKeys';\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Liveness key an executor refreshes; callers check it before publishing. */\nexport function heartbeatKey(executorId: string): string {\n return redisKey('rr', 'hb', executorId);\n}\n\n/** Pub/sub channel: executors subscribe; callers publish the JSON request (see publishedRequestSchema). */\nexport function requestChannel(executorId: string): string {\n return redisKey('rr', 'req', executorId);\n}\n\n/** Per-request key the executor SETs the JSONReply to; the caller polls it with GETDEL. */\nexport function replyKey(requestId: string): string {\n return redisKey('rr', 'reply', requestId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAyB;AAElB,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;AAGO,SAAS,aAAa,YAA4B;AACvD,aAAO,2BAAS,MAAM,MAAM,UAAU;AACxC;AAGO,SAAS,eAAe,YAA4B;AACzD,aAAO,2BAAS,MAAM,OAAO,UAAU;AACzC;AAGO,SAAS,SAAS,WAA2B;AAClD,aAAO,2BAAS,MAAM,SAAS,SAAS;AAC1C;","names":[]}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
// src/request-reply/utils.ts
|
|
2
|
+
import { redisKey } from "../core/redisKeys.mjs";
|
|
2
3
|
function sleep(ms) {
|
|
3
4
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
5
|
}
|
|
5
|
-
var KEY_PREFIX = "rr";
|
|
6
6
|
function heartbeatKey(executorId) {
|
|
7
|
-
return
|
|
7
|
+
return redisKey("rr", "hb", executorId);
|
|
8
8
|
}
|
|
9
9
|
function requestChannel(executorId) {
|
|
10
|
-
return
|
|
10
|
+
return redisKey("rr", "req", executorId);
|
|
11
11
|
}
|
|
12
12
|
function replyKey(requestId) {
|
|
13
|
-
return
|
|
13
|
+
return redisKey("rr", "reply", requestId);
|
|
14
14
|
}
|
|
15
15
|
export {
|
|
16
|
-
KEY_PREFIX,
|
|
17
16
|
heartbeatKey,
|
|
18
17
|
replyKey,
|
|
19
18
|
requestChannel,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/request-reply/utils.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/request-reply/utils.ts"],"sourcesContent":["import { redisKey } from '../core/redisKeys';\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Liveness key an executor refreshes; callers check it before publishing. */\nexport function heartbeatKey(executorId: string): string {\n return redisKey('rr', 'hb', executorId);\n}\n\n/** Pub/sub channel: executors subscribe; callers publish the JSON request (see publishedRequestSchema). */\nexport function requestChannel(executorId: string): string {\n return redisKey('rr', 'req', executorId);\n}\n\n/** Per-request key the executor SETs the JSONReply to; the caller polls it with GETDEL. */\nexport function replyKey(requestId: string): string {\n return redisKey('rr', 'reply', requestId);\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAElB,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;AAGO,SAAS,aAAa,YAA4B;AACvD,SAAO,SAAS,MAAM,MAAM,UAAU;AACxC;AAGO,SAAS,eAAe,YAA4B;AACzD,SAAO,SAAS,MAAM,OAAO,UAAU;AACzC;AAGO,SAAS,SAAS,WAA2B;AAClD,SAAO,SAAS,MAAM,SAAS,SAAS;AAC1C;","names":[]}
|