@truefoundry/trueforge-core 0.2.0 → 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.
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/llm/LLMTypes.js +2 -2
- package/dist/core/llm/LLMTypes.js.map +1 -1
- package/dist/core/llm/LLMTypes.mjs +2 -2
- package/dist/core/llm/LLMTypes.mjs.map +1 -1
- package/dist/core/llm/VercelAILLM.d.ts.map +1 -1
- package/dist/core/llm/VercelAILLM.js +10 -0
- package/dist/core/llm/VercelAILLM.js.map +1 -1
- package/dist/core/llm/VercelAILLM.mjs +10 -0
- package/dist/core/llm/VercelAILLM.mjs.map +1 -1
- package/dist/core/llm/toOpenAIChatMessage.d.ts +4 -3
- package/dist/core/llm/toOpenAIChatMessage.d.ts.map +1 -1
- package/dist/core/llm/toOpenAIChatMessage.js.map +1 -1
- package/dist/core/llm/toOpenAIChatMessage.mjs.map +1 -1
- package/dist/core/mcp/RemoteMCP.d.ts +2 -1
- package/dist/core/mcp/RemoteMCP.d.ts.map +1 -1
- package/dist/core/mcp/RemoteMCP.js +65 -20
- package/dist/core/mcp/RemoteMCP.js.map +1 -1
- package/dist/core/mcp/RemoteMCP.mjs +65 -20
- package/dist/core/mcp/RemoteMCP.mjs.map +1 -1
- package/dist/core/mcp/remoteMcpClient.d.ts.map +1 -1
- package/dist/core/mcp/remoteMcpClient.js +2 -5
- package/dist/core/mcp/remoteMcpClient.js.map +1 -1
- package/dist/core/mcp/remoteMcpClient.mjs +2 -5
- package/dist/core/mcp/remoteMcpClient.mjs.map +1 -1
- package/dist/core/runtime/AgentThread.d.ts.map +1 -1
- package/dist/core/runtime/AgentThread.js +7 -1
- package/dist/core/runtime/AgentThread.js.map +1 -1
- package/dist/core/runtime/AgentThread.mjs +7 -1
- package/dist/core/runtime/AgentThread.mjs.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.js +56 -14
- package/dist/core/sandbox/provider/TFYSandboxProvider.js.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs +57 -16
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs.map +1 -1
- package/dist/core/util/errorLogFields.d.ts +1 -0
- package/dist/core/util/errorLogFields.d.ts.map +1 -1
- package/dist/core/util/errorLogFields.js +33 -6
- package/dist/core/util/errorLogFields.js.map +1 -1
- package/dist/core/util/errorLogFields.mjs +33 -6
- package/dist/core/util/errorLogFields.mjs.map +1 -1
- package/dist/core/util/ssrfGuard.d.ts +10 -0
- package/dist/core/util/ssrfGuard.d.ts.map +1 -0
- package/dist/core/util/ssrfGuard.js +333 -0
- package/dist/core/util/ssrfGuard.js.map +1 -0
- package/dist/core/util/ssrfGuard.mjs +305 -0
- package/dist/core/util/ssrfGuard.mjs.map +1 -0
- package/dist/core/web-search/ParallelWebSearchProvider.d.ts.map +1 -1
- package/dist/core/web-search/ParallelWebSearchProvider.js +5 -1
- package/dist/core/web-search/ParallelWebSearchProvider.js.map +1 -1
- package/dist/core/web-search/ParallelWebSearchProvider.mjs +5 -1
- package/dist/core/web-search/ParallelWebSearchProvider.mjs.map +1 -1
- package/dist/request-reply/client.d.ts +2 -2
- package/dist/request-reply/client.d.ts.map +1 -1
- package/dist/request-reply/client.js.map +1 -1
- package/dist/request-reply/client.mjs.map +1 -1
- package/dist/request-reply/executor.d.ts +3 -3
- package/dist/request-reply/executor.d.ts.map +1 -1
- package/dist/request-reply/executor.js +0 -1
- package/dist/request-reply/executor.js.map +1 -1
- package/dist/request-reply/executor.mjs +0 -1
- package/dist/request-reply/executor.mjs.map +1 -1
- package/dist/request-reply/index.d.ts +1 -0
- package/dist/request-reply/index.d.ts.map +1 -1
- package/dist/request-reply/index.js.map +1 -1
- package/dist/request-reply/index.mjs.map +1 -1
- package/dist/request-reply/redisClient.d.ts +4 -0
- package/dist/request-reply/redisClient.d.ts.map +1 -0
- package/dist/request-reply/redisClient.js +19 -0
- package/dist/request-reply/redisClient.js.map +1 -0
- package/dist/request-reply/redisClient.mjs +1 -0
- package/dist/request-reply/redisClient.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/util/ssrfGuard.ts"],"sourcesContent":["import type { LookupAllOptions, LookupOptions } from 'node:dns';\nimport { lookup as dnsLookup } from 'node:dns';\nimport { lookup as dnsLookupAsync } from 'node:dns/promises';\nimport { BlockList, isIP, type LookupFunction } from 'node:net';\nimport { Agent, fetch as undiciFetch } from 'undici';\n\nlet allowedHosts: string[] = [];\nlet blockedHosts: string[] = [];\nlet guardEnabled = true;\n\nconst URL_VERIFY = {\n allowedProtocols: ['http:', 'https:'],\n denyCidrsV4: [\n '0.0.0.0/8', // this host\n '10.0.0.0/8', // private\n '100.64.0.0/10', // CGNAT (EKS secondary pod CIDRs)\n '127.0.0.0/8', // loopback\n '169.254.0.0/16', // link-local + metadata\n '172.16.0.0/12', // private (docker, k8s service CIDRs)\n '192.0.0.0/24', // IETF protocol assignments\n '192.0.2.0/24', // TEST-NET-1\n '192.88.99.0/24', // 6to4 relay anycast\n '192.168.0.0/16', // private\n '198.18.0.0/15', // benchmarking\n '198.51.100.0/24', // TEST-NET-2\n '203.0.113.0/24', // TEST-NET-3\n '224.0.0.0/4', // multicast\n '240.0.0.0/4', // reserved + broadcast\n ],\n denyCidrsV6: [\n '::/96', // unspecified, ::1, IPv4-compatible\n '64:ff9b::/96', // NAT64 well-known\n '64:ff9b:1::/48', // NAT64 local-use\n '100::/64', // discard-only\n '2001::/32', // Teredo\n '2001:10::/28', // ORCHID\n '2001:20::/28', // ORCHIDv2\n '2001:db8::/32', // documentation\n '2002::/16', // 6to4\n 'fc00::/7', // unique-local (IPv6 k8s service CIDRs)\n 'fe80::/10', // link-local\n 'ff00::/8', // multicast\n ],\n denyHostSuffixes: [\n '.local',\n '.localhost',\n '.localdomain',\n '.internal',\n '.svc',\n '.cluster',\n '.arpa',\n '.lan',\n '.intranet',\n '.corp',\n '.home',\n '.test',\n '.invalid',\n '.example',\n ],\n};\n\nconst privateNets = new BlockList();\nfunction addDenyCidrs(cidrs: readonly string[], family: 'ipv4' | 'ipv6'): void {\n for (const cidr of cidrs) {\n const slash = cidr.lastIndexOf('/');\n privateNets.addSubnet(cidr.slice(0, slash), Number(cidr.slice(slash + 1)), family);\n }\n}\naddDenyCidrs(URL_VERIFY.denyCidrsV4, 'ipv4');\naddDenyCidrs(URL_VERIFY.denyCidrsV6, 'ipv6');\n\nconst MAX_REDIRECTS = 20;\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\nconst CROSS_ORIGIN_STRIPPED_HEADERS = ['authorization', 'proxy-authorization', 'cookie', 'host'];\n\nexport function configureOutboundUrlGuard(config: {\n enabled?: boolean;\n allowedHosts: readonly string[];\n blockedHosts: readonly string[];\n}): void {\n guardEnabled = config.enabled ?? true;\n allowedHosts = config.allowedHosts.map(normalizeHost);\n blockedHosts = config.blockedHosts.map(normalizeHost);\n}\n\nfunction normalizeHost(hostname: string): string {\n const host = hostname.replace(/\\.$/, '').toLowerCase();\n return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;\n}\n\nfunction isPrivateIp(address: string): boolean {\n const ip = address.replace(/^::ffff:/i, '');\n if (isIP(ip) === 4) {\n return privateNets.check(ip, 'ipv4');\n }\n if (isIP(ip) === 6) {\n return privateNets.check(ip, 'ipv6');\n }\n return true;\n}\n\nfunction blockedError(host: string, cause?: unknown): Error {\n return new Error(`Outbound URL blocked for host \"${host}\"`, { cause });\n}\n\nfunction deny(host: string, cause?: unknown): never {\n throw blockedError(host, cause);\n}\n\nfunction assertHost(host: string): void {\n if (host === '' || blockedHosts.includes(host)) {\n deny(host);\n }\n if (allowedHosts.includes(host)) {\n return;\n }\n if (isIP(host) === 0) {\n if (!host.includes('.') || URL_VERIFY.denyHostSuffixes.some(suffix => host.endsWith(suffix))) {\n deny(host);\n }\n return;\n }\n if (isPrivateIp(host)) {\n deny(host);\n }\n}\n\nfunction parseOutboundUrl(input: string | URL | Request): URL {\n let url: URL;\n try {\n url = new URL(input instanceof Request ? input.url : input);\n } catch (error) {\n throw new Error('Outbound URL blocked', { cause: error });\n }\n if (!URL_VERIFY.allowedProtocols.includes(url.protocol)) {\n throw new Error('Outbound URL blocked: only http and https are allowed');\n }\n return url;\n}\n\n/** undici runs this as the socket lookup, so the addresses we allow are the ones connected to. */\nconst guardedLookup: LookupFunction = (hostname, options: LookupOptions, callback) => {\n if (!guardEnabled) {\n dnsLookup(hostname, options, callback);\n return;\n }\n const host = normalizeHost(hostname);\n try {\n assertHost(host);\n } catch (error) {\n callback(error instanceof Error ? error : blockedError(host, error), '');\n return;\n }\n if (allowedHosts.includes(host) || isIP(host) !== 0) {\n dnsLookup(hostname, options, callback);\n return;\n }\n const allOptions: LookupAllOptions = { ...options, all: true };\n dnsLookup(hostname, allOptions, (err, addresses) => {\n if (err) {\n callback(err, '');\n return;\n }\n const first = addresses[0];\n if (first === undefined || addresses.some(record => isPrivateIp(record.address))) {\n callback(blockedError(host), '');\n return;\n }\n if (options.all) {\n callback(null, addresses);\n return;\n }\n callback(null, first.address, first.family);\n });\n};\n\nconst MCP_BODY_TIMEOUT_MS = 30 * 60 * 1000;\n\nconst outboundAgent = new Agent({\n connect: { lookup: guardedLookup },\n});\n\nconst mcpOutboundAgent = new Agent({\n // MCP SSE/streamable-HTTP stays idle between tool calls; undici's 300s bodyTimeout kills it.\n bodyTimeout: MCP_BODY_TIMEOUT_MS,\n connect: { lookup: guardedLookup },\n});\n\nexport async function assertSafeOutboundUrl(input: string | URL | Request): Promise<void> {\n const url = parseOutboundUrl(input);\n if (!guardEnabled) {\n return;\n }\n const host = normalizeHost(url.hostname);\n assertHost(host);\n if (allowedHosts.includes(host) || isIP(host) !== 0) {\n return;\n }\n let addresses: string[];\n try {\n addresses = (await dnsLookupAsync(host, { all: true })).map(record => record.address);\n } catch (error) {\n deny(host, error);\n }\n if (addresses.some(isPrivateIp)) {\n deny(host);\n }\n}\n\nfunction nextHop(\n response: Response,\n location: string,\n current: URL,\n init: RequestInit,\n): { url: URL; init: RequestInit } {\n let nextUrl: URL;\n try {\n nextUrl = new URL(location, current);\n } catch (error) {\n throw new Error('Outbound URL blocked', { cause: error });\n }\n if (!URL_VERIFY.allowedProtocols.includes(nextUrl.protocol)) {\n throw new Error('Outbound URL blocked: only http and https are allowed');\n }\n\n const headers = new Headers(init.headers);\n let method = (init.method ?? 'GET').toUpperCase();\n let body = init.body ?? null;\n const downgradesToGet =\n ((response.status === 301 || response.status === 302) && method === 'POST') ||\n (response.status === 303 && method !== 'GET' && method !== 'HEAD');\n if (downgradesToGet) {\n method = 'GET';\n body = null;\n headers.delete('content-encoding');\n headers.delete('content-language');\n headers.delete('content-location');\n headers.delete('content-type');\n headers.delete('content-length');\n }\n if (nextUrl.origin !== current.origin) {\n for (const header of CROSS_ORIGIN_STRIPPED_HEADERS) {\n headers.delete(header);\n }\n }\n return { url: nextUrl, init: { ...init, method, headers, body } };\n}\n\nfunction mergeRequestInit(input: string | URL | Request, init: RequestInit): RequestInit {\n if (!(input instanceof Request)) {\n return init;\n }\n return {\n method: input.method,\n headers: input.headers,\n body: input.body,\n signal: input.signal,\n redirect: input.redirect,\n ...init,\n };\n}\n\nasync function guardedFetch(\n input: string | URL | Request,\n init: RequestInit,\n hopsLeft: number,\n agent: Agent,\n): Promise<Response> {\n const url = parseOutboundUrl(input);\n if (guardEnabled) {\n assertHost(normalizeHost(url.hostname));\n }\n const merged = mergeRequestInit(input, init);\n const redirect = merged.redirect ?? 'follow';\n const followsRedirects = guardEnabled && redirect === 'follow';\n const requestInit = {\n redirect: followsRedirects ? 'manual' : redirect,\n dispatcher: agent,\n };\n // npm undici vs @types/node undici-types: FormData/Headers do not line up under exactOptionalPropertyTypes.\n Object.assign(requestInit, merged, {\n redirect: followsRedirects ? 'manual' : redirect,\n dispatcher: agent,\n ...(merged.body != null && typeof merged.body === 'object' && 'getReader' in merged.body\n ? { duplex: 'half' as const }\n : {}),\n });\n const response = await undiciFetch(url.href, requestInit);\n if (!followsRedirects) {\n return response;\n }\n if (!REDIRECT_STATUSES.has(response.status)) {\n return response;\n }\n const location = response.headers.get('location');\n if (location === null) {\n return response;\n }\n void response.body?.cancel().catch(() => undefined);\n if (hopsLeft === 0) {\n throw new Error('Outbound URL blocked: too many redirects');\n }\n const hop = nextHop(response, location, url, merged);\n return guardedFetch(hop.url, hop.init, hopsLeft - 1, agent);\n}\n\nexport async function ssrfFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {\n return guardedFetch(input, init ?? {}, MAX_REDIRECTS, outboundAgent);\n}\n\n/** Same as `ssrfFetch` with a 30m bodyTimeout for idle MCP SSE / streamable-HTTP. */\nexport async function mcpSsrfFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {\n return guardedFetch(input, init ?? {}, MAX_REDIRECTS, mcpOutboundAgent);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ParallelWebSearchProvider.d.ts","sourceRoot":"","sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ParallelWebSearchProvider.d.ts","sourceRoot":"","sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AAOzE,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,IAAI,EAAE,kBAAkB,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;;IAClE,QAAQ,CAAC,EAAE,+BAA+B;IAI1C,YAAY,OAAO,EAAE,gCAAgC,EAMpD;IAEK,MAAM,CAAC,KAAK,EAAE;QAAE,cAAc,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBvG;IAEK,KAAK,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAoB5F;CACF"}
|
|
@@ -34,6 +34,7 @@ __export(ParallelWebSearchProvider_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(ParallelWebSearchProvider_exports);
|
|
36
36
|
var import_parallel_web = __toESM(require("parallel-web"), 1);
|
|
37
|
+
var import_ssrfGuard = require("../util/ssrfGuard.js");
|
|
37
38
|
var import_WebSearchProvider = require("./WebSearchProvider.js");
|
|
38
39
|
var PARALLEL_SEARCH_MAX_RESULTS = 5;
|
|
39
40
|
var PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500;
|
|
@@ -43,7 +44,10 @@ var ParallelWebSearchProvider = class {
|
|
|
43
44
|
#client;
|
|
44
45
|
#mode;
|
|
45
46
|
constructor(options) {
|
|
46
|
-
this.#client = new import_parallel_web.default({
|
|
47
|
+
this.#client = new import_parallel_web.default({
|
|
48
|
+
apiKey: options.apiKey,
|
|
49
|
+
fetch: import_ssrfGuard.ssrfFetch
|
|
50
|
+
});
|
|
47
51
|
this.#mode = options.mode;
|
|
48
52
|
}
|
|
49
53
|
async search(input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"sourcesContent":["import Parallel from 'parallel-web';\nimport {\n WebSearchProviders,\n type IWebSearchProvider,\n type WebFetchPages,\n type WebSearchHits,\n} from './WebSearchProvider';\n\nexport type ParallelSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced';\n\n/** Keep search payloads under the large-tool-response individual threshold (~6k tokens). */\nconst PARALLEL_SEARCH_MAX_RESULTS = 5;\nconst PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500;\nconst PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000;\n\nexport interface ParallelWebSearchProviderOptions {\n apiKey: string;\n /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */\n mode: ParallelSearchMode;\n}\n\n/**\n * Parallel Search + Extract via the official `parallel-web` SDK.\n */\nexport class ParallelWebSearchProvider implements IWebSearchProvider {\n readonly id = WebSearchProviders.Parallel;\n readonly #client: Parallel;\n readonly #mode: ParallelSearchMode;\n\n constructor(options: ParallelWebSearchProviderOptions) {\n this.#client = new Parallel({
|
|
1
|
+
{"version":3,"sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"sourcesContent":["import Parallel from 'parallel-web';\nimport { ssrfFetch } from '../util/ssrfGuard';\nimport {\n WebSearchProviders,\n type IWebSearchProvider,\n type WebFetchPages,\n type WebSearchHits,\n} from './WebSearchProvider';\n\nexport type ParallelSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced';\n\n/** Keep search payloads under the large-tool-response individual threshold (~6k tokens). */\nconst PARALLEL_SEARCH_MAX_RESULTS = 5;\nconst PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500;\nconst PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000;\n\nexport interface ParallelWebSearchProviderOptions {\n apiKey: string;\n /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */\n mode: ParallelSearchMode;\n}\n\n/**\n * Parallel Search + Extract via the official `parallel-web` SDK.\n */\nexport class ParallelWebSearchProvider implements IWebSearchProvider {\n readonly id = WebSearchProviders.Parallel;\n readonly #client: Parallel;\n readonly #mode: ParallelSearchMode;\n\n constructor(options: ParallelWebSearchProviderOptions) {\n this.#client = new Parallel({\n apiKey: options.apiKey,\n fetch: ssrfFetch,\n });\n this.#mode = options.mode;\n }\n\n async search(input: { search_queries: string[]; objective: string | undefined }): Promise<WebSearchHits> {\n const response = await this.#client.search({\n search_queries: input.search_queries,\n ...(input.objective ? { objective: input.objective } : {}),\n mode: this.#mode,\n max_chars_total: PARALLEL_SEARCH_MAX_CHARS_TOTAL,\n advanced_settings: {\n max_results: PARALLEL_SEARCH_MAX_RESULTS,\n excerpt_settings: { max_chars_per_result: PARALLEL_SEARCH_MAX_CHARS_PER_RESULT },\n },\n });\n return {\n hits: response.results.map(result => ({\n title: result.title ?? '',\n url: result.url,\n snippet: result.excerpts.join('\\n\\n'),\n published_at: result.publish_date ?? null,\n })),\n };\n }\n\n async fetch(input: { urls: string[]; objective: string | undefined }): Promise<WebFetchPages> {\n const response = await this.#client.extract({\n urls: input.urls,\n ...(input.objective ? { objective: input.objective } : {}),\n });\n const pages = [\n ...response.results.map(result => ({\n url: result.url,\n title: result.title ?? null,\n content: result.full_content ?? result.excerpts.join('\\n\\n'),\n error: null,\n })),\n ...response.errors.map(err => ({\n url: err.url,\n title: null,\n content: '',\n error: err.content ?? err.error_type,\n })),\n ];\n return { pages };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAqB;AACrB,uBAA0B;AAC1B,+BAKO;AAKP,IAAM,8BAA8B;AACpC,IAAM,uCAAuC;AAC7C,IAAM,kCAAkC;AAWjC,IAAM,4BAAN,MAA8D;AAAA,EAC1D,KAAK,4CAAmB;AAAA,EACxB;AAAA,EACA;AAAA,EAET,YAAY,SAA2C;AACrD,SAAK,UAAU,IAAI,oBAAAA,QAAS;AAAA,MAC1B,QAAQ,QAAQ;AAAA,MAChB,OAAO;AAAA,IACT,CAAC;AACD,SAAK,QAAQ,QAAQ;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,OAA4F;AACvG,UAAM,WAAW,MAAM,KAAK,QAAQ,OAAO;AAAA,MACzC,gBAAgB,MAAM;AAAA,MACtB,GAAI,MAAM,YAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,MACxD,MAAM,KAAK;AAAA,MACX,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,QACjB,aAAa;AAAA,QACb,kBAAkB,EAAE,sBAAsB,qCAAqC;AAAA,MACjF;AAAA,IACF,CAAC;AACD,WAAO;AAAA,MACL,MAAM,SAAS,QAAQ,IAAI,aAAW;AAAA,QACpC,OAAO,OAAO,SAAS;AAAA,QACvB,KAAK,OAAO;AAAA,QACZ,SAAS,OAAO,SAAS,KAAK,MAAM;AAAA,QACpC,cAAc,OAAO,gBAAgB;AAAA,MACvC,EAAE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,OAAkF;AAC5F,UAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ;AAAA,MAC1C,MAAM,MAAM;AAAA,MACZ,GAAI,MAAM,YAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IAC1D,CAAC;AACD,UAAM,QAAQ;AAAA,MACZ,GAAG,SAAS,QAAQ,IAAI,aAAW;AAAA,QACjC,KAAK,OAAO;AAAA,QACZ,OAAO,OAAO,SAAS;AAAA,QACvB,SAAS,OAAO,gBAAgB,OAAO,SAAS,KAAK,MAAM;AAAA,QAC3D,OAAO;AAAA,MACT,EAAE;AAAA,MACF,GAAG,SAAS,OAAO,IAAI,UAAQ;AAAA,QAC7B,KAAK,IAAI;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,OAAO,IAAI,WAAW,IAAI;AAAA,MAC5B,EAAE;AAAA,IACJ;AACA,WAAO,EAAE,MAAM;AAAA,EACjB;AACF;","names":["Parallel"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/core/web-search/ParallelWebSearchProvider.ts
|
|
2
2
|
import Parallel from "parallel-web";
|
|
3
|
+
import { ssrfFetch } from "../util/ssrfGuard.mjs";
|
|
3
4
|
import {
|
|
4
5
|
WebSearchProviders
|
|
5
6
|
} from "./WebSearchProvider.mjs";
|
|
@@ -11,7 +12,10 @@ var ParallelWebSearchProvider = class {
|
|
|
11
12
|
#client;
|
|
12
13
|
#mode;
|
|
13
14
|
constructor(options) {
|
|
14
|
-
this.#client = new Parallel({
|
|
15
|
+
this.#client = new Parallel({
|
|
16
|
+
apiKey: options.apiKey,
|
|
17
|
+
fetch: ssrfFetch
|
|
18
|
+
});
|
|
15
19
|
this.#mode = options.mode;
|
|
16
20
|
}
|
|
17
21
|
async search(input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"sourcesContent":["import Parallel from 'parallel-web';\nimport {\n WebSearchProviders,\n type IWebSearchProvider,\n type WebFetchPages,\n type WebSearchHits,\n} from './WebSearchProvider';\n\nexport type ParallelSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced';\n\n/** Keep search payloads under the large-tool-response individual threshold (~6k tokens). */\nconst PARALLEL_SEARCH_MAX_RESULTS = 5;\nconst PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500;\nconst PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000;\n\nexport interface ParallelWebSearchProviderOptions {\n apiKey: string;\n /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */\n mode: ParallelSearchMode;\n}\n\n/**\n * Parallel Search + Extract via the official `parallel-web` SDK.\n */\nexport class ParallelWebSearchProvider implements IWebSearchProvider {\n readonly id = WebSearchProviders.Parallel;\n readonly #client: Parallel;\n readonly #mode: ParallelSearchMode;\n\n constructor(options: ParallelWebSearchProviderOptions) {\n this.#client = new Parallel({
|
|
1
|
+
{"version":3,"sources":["../../../src/core/web-search/ParallelWebSearchProvider.ts"],"sourcesContent":["import Parallel from 'parallel-web';\nimport { ssrfFetch } from '../util/ssrfGuard';\nimport {\n WebSearchProviders,\n type IWebSearchProvider,\n type WebFetchPages,\n type WebSearchHits,\n} from './WebSearchProvider';\n\nexport type ParallelSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced';\n\n/** Keep search payloads under the large-tool-response individual threshold (~6k tokens). */\nconst PARALLEL_SEARCH_MAX_RESULTS = 5;\nconst PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500;\nconst PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000;\n\nexport interface ParallelWebSearchProviderOptions {\n apiKey: string;\n /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */\n mode: ParallelSearchMode;\n}\n\n/**\n * Parallel Search + Extract via the official `parallel-web` SDK.\n */\nexport class ParallelWebSearchProvider implements IWebSearchProvider {\n readonly id = WebSearchProviders.Parallel;\n readonly #client: Parallel;\n readonly #mode: ParallelSearchMode;\n\n constructor(options: ParallelWebSearchProviderOptions) {\n this.#client = new Parallel({\n apiKey: options.apiKey,\n fetch: ssrfFetch,\n });\n this.#mode = options.mode;\n }\n\n async search(input: { search_queries: string[]; objective: string | undefined }): Promise<WebSearchHits> {\n const response = await this.#client.search({\n search_queries: input.search_queries,\n ...(input.objective ? { objective: input.objective } : {}),\n mode: this.#mode,\n max_chars_total: PARALLEL_SEARCH_MAX_CHARS_TOTAL,\n advanced_settings: {\n max_results: PARALLEL_SEARCH_MAX_RESULTS,\n excerpt_settings: { max_chars_per_result: PARALLEL_SEARCH_MAX_CHARS_PER_RESULT },\n },\n });\n return {\n hits: response.results.map(result => ({\n title: result.title ?? '',\n url: result.url,\n snippet: result.excerpts.join('\\n\\n'),\n published_at: result.publish_date ?? null,\n })),\n };\n }\n\n async fetch(input: { urls: string[]; objective: string | undefined }): Promise<WebFetchPages> {\n const response = await this.#client.extract({\n urls: input.urls,\n ...(input.objective ? { objective: input.objective } : {}),\n });\n const pages = [\n ...response.results.map(result => ({\n url: result.url,\n title: result.title ?? null,\n content: result.full_content ?? result.excerpts.join('\\n\\n'),\n error: null,\n })),\n ...response.errors.map(err => ({\n url: err.url,\n title: null,\n content: '',\n error: err.content ?? err.error_type,\n })),\n ];\n return { pages };\n }\n}\n"],"mappings":";AAAA,OAAO,cAAc;AACrB,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,OAIK;AAKP,IAAM,8BAA8B;AACpC,IAAM,uCAAuC;AAC7C,IAAM,kCAAkC;AAWjC,IAAM,4BAAN,MAA8D;AAAA,EAC1D,KAAK,mBAAmB;AAAA,EACxB;AAAA,EACA;AAAA,EAET,YAAY,SAA2C;AACrD,SAAK,UAAU,IAAI,SAAS;AAAA,MAC1B,QAAQ,QAAQ;AAAA,MAChB,OAAO;AAAA,IACT,CAAC;AACD,SAAK,QAAQ,QAAQ;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,OAA4F;AACvG,UAAM,WAAW,MAAM,KAAK,QAAQ,OAAO;AAAA,MACzC,gBAAgB,MAAM;AAAA,MACtB,GAAI,MAAM,YAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,MACxD,MAAM,KAAK;AAAA,MACX,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,QACjB,aAAa;AAAA,QACb,kBAAkB,EAAE,sBAAsB,qCAAqC;AAAA,MACjF;AAAA,IACF,CAAC;AACD,WAAO;AAAA,MACL,MAAM,SAAS,QAAQ,IAAI,aAAW;AAAA,QACpC,OAAO,OAAO,SAAS;AAAA,QACvB,KAAK,OAAO;AAAA,QACZ,SAAS,OAAO,SAAS,KAAK,MAAM;AAAA,QACpC,cAAc,OAAO,gBAAgB;AAAA,MACvC,EAAE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,OAAkF;AAC5F,UAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ;AAAA,MAC1C,MAAM,MAAM;AAAA,MACZ,GAAI,MAAM,YAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IAC1D,CAAC;AACD,UAAM,QAAQ;AAAA,MACZ,GAAG,SAAS,QAAQ,IAAI,aAAW;AAAA,QACjC,KAAK,OAAO;AAAA,QACZ,OAAO,OAAO,SAAS;AAAA,QACvB,SAAS,OAAO,gBAAgB,OAAO,SAAS,KAAK,MAAM;AAAA,QAC3D,OAAO;AAAA,MACT,EAAE;AAAA,MACF,GAAG,SAAS,OAAO,IAAI,UAAQ;AAAA,QAC7B,KAAK,IAAI;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,OAAO,IAAI,WAAW,IAAI;AAAA,MAC5B,EAAE;AAAA,IACJ;AACA,WAAO,EAAE,MAAM;AAAA,EACjB;AACF;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RedisClient } from './redisClient';
|
|
2
2
|
import type { JSONReply, JSONValue, RequestEnvelope } from './types';
|
|
3
3
|
export interface SendRequestOptions {
|
|
4
4
|
/** Max time to wait for a reply after the request is published (ms). */
|
|
@@ -12,7 +12,7 @@ export interface SendRequestOptions {
|
|
|
12
12
|
* heartbeat expires, or the wait budget is exceeded.
|
|
13
13
|
*/
|
|
14
14
|
export declare function redisRequest<T extends JSONValue>({ redis: redisClient, executorId, path, request, options, }: {
|
|
15
|
-
redis:
|
|
15
|
+
redis: RedisClient;
|
|
16
16
|
executorId: string;
|
|
17
17
|
path: string;
|
|
18
18
|
request: RequestEnvelope<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/request-reply/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/request-reply/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAoB,eAAe,EAAE,MAAM,SAAS,CAAC;AAIvF,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,cAAc,EAAE,MAAM,CAAC;CACxB;AAyBD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,CAAC,SAAS,SAAS,EAAE,EACtD,KAAK,EAAE,WAAW,EAClB,UAAU,EACV,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;CACnD,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/request-reply/client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { performance } from 'node:perf_hooks';\nimport
|
|
1
|
+
{"version":3,"sources":["../../src/request-reply/client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { performance } from 'node:perf_hooks';\nimport { NoResponderError, RequestTimeoutError } from './errors';\nimport type { RedisClient } from './redisClient';\nimport type { JSONReply, JSONValue, PublishedRequest, RequestEnvelope } from './types';\nimport { jsonReplySchema } from './types';\nimport { heartbeatKey, replyKey, requestChannel, sleep } from './utils';\n\nexport interface SendRequestOptions {\n /** Max time to wait for a reply after the request is published (ms). */\n replyTimeoutMs: number;\n /** Sleep between poll attempts when the reply is not yet present (ms). */\n pollIntervalMs: number;\n}\n\nconst DEFAULT_REPLY_TIMEOUT_MS = 60_000;\nconst DEFAULT_POLL_INTERVAL_MS = 500;\n\nfunction resolveSendRequestOptions(options: Partial<SendRequestOptions> | undefined): SendRequestOptions {\n return {\n replyTimeoutMs: options?.replyTimeoutMs ?? DEFAULT_REPLY_TIMEOUT_MS,\n pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,\n };\n}\n\nfunction parseReplyPayload(raw: string) {\n const parsed: unknown = JSON.parse(raw);\n return jsonReplySchema.parse(parsed);\n}\n\nasync function getDelReply(redisClient: RedisClient, rKey: string): Promise<JSONReply | null> {\n const raw = await redisClient.getDel(rKey);\n if (raw === null) {\n return null;\n }\n return parseReplyPayload(raw);\n}\n\n/**\n * Publishes the request (JSON) on the worker channel, then polls the reply key with GETDEL\n * (see https://redis.io/docs/latest/commands/getdel/ ) until a result arrives, the executor\n * heartbeat expires, or the wait budget is exceeded.\n */\nexport async function redisRequest<T extends JSONValue>({\n redis: redisClient,\n executorId,\n path,\n request,\n options,\n}: {\n redis: RedisClient;\n executorId: string;\n path: string;\n request: RequestEnvelope<T>;\n options?: Partial<SendRequestOptions> | undefined;\n}): Promise<JSONReply> {\n const { replyTimeoutMs, pollIntervalMs } = resolveSendRequestOptions(options);\n const aliveK = heartbeatKey(executorId);\n if ((await redisClient.exists(aliveK)) === 0) {\n throw new NoResponderError(executorId);\n }\n\n const requestId = randomUUID();\n const rKey = replyKey(requestId);\n const payload: PublishedRequest = {\n replyKey: rKey,\n path,\n body: request.body,\n headers: request.headers,\n };\n const serializedPayload = JSON.stringify(payload);\n const subscriberCount = await redisClient.publish(requestChannel(executorId), serializedPayload);\n if (subscriberCount === 0) {\n throw new NoResponderError(executorId, 'No executor received the published request');\n }\n\n const replyDeadline = performance.now() + replyTimeoutMs;\n // Give the executor a moment to process before the first poll.\n await sleep(Math.min(200, replyTimeoutMs));\n for (;;) {\n const replyPayload = await getDelReply(redisClient, rKey);\n if (replyPayload) {\n return replyPayload;\n }\n // We already checked that the executor was alive before sending, but it may have\n // died while we wait. Recheck here so we fail soon instead of waiting until the timeout.\n if ((await redisClient.exists(aliveK)) === 0) {\n throw new NoResponderError(executorId);\n }\n if (performance.now() >= replyDeadline) {\n throw new RequestTimeoutError(replyTimeoutMs);\n }\n await sleep(pollIntervalMs);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA2B;AAC3B,6BAA4B;AAC5B,oBAAsD;AAGtD,mBAAgC;AAChC,mBAA8D;AAS9D,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AAEjC,SAAS,0BAA0B,SAAsE;AACvG,SAAO;AAAA,IACL,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,gBAAgB,SAAS,kBAAkB;AAAA,EAC7C;AACF;AAEA,SAAS,kBAAkB,KAAa;AACtC,QAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,SAAO,6BAAgB,MAAM,MAAM;AACrC;AAEA,eAAe,YAAY,aAA0B,MAAyC;AAC5F,QAAM,MAAM,MAAM,YAAY,OAAO,IAAI;AACzC,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,GAAG;AAC9B;AAOA,eAAsB,aAAkC;AAAA,EACtD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMuB;AACrB,QAAM,EAAE,gBAAgB,eAAe,IAAI,0BAA0B,OAAO;AAC5E,QAAM,aAAS,2BAAa,UAAU;AACtC,MAAK,MAAM,YAAY,OAAO,MAAM,MAAO,GAAG;AAC5C,UAAM,IAAI,+BAAiB,UAAU;AAAA,EACvC;AAEA,QAAM,gBAAY,+BAAW;AAC7B,QAAM,WAAO,uBAAS,SAAS;AAC/B,QAAM,UAA4B;AAAA,IAChC,UAAU;AAAA,IACV;AAAA,IACA,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,EACnB;AACA,QAAM,oBAAoB,KAAK,UAAU,OAAO;AAChD,QAAM,kBAAkB,MAAM,YAAY,YAAQ,6BAAe,UAAU,GAAG,iBAAiB;AAC/F,MAAI,oBAAoB,GAAG;AACzB,UAAM,IAAI,+BAAiB,YAAY,4CAA4C;AAAA,EACrF;AAEA,QAAM,gBAAgB,mCAAY,IAAI,IAAI;AAE1C,YAAM,oBAAM,KAAK,IAAI,KAAK,cAAc,CAAC;AACzC,aAAS;AACP,UAAM,eAAe,MAAM,YAAY,aAAa,IAAI;AACxD,QAAI,cAAc;AAChB,aAAO;AAAA,IACT;AAGA,QAAK,MAAM,YAAY,OAAO,MAAM,MAAO,GAAG;AAC5C,YAAM,IAAI,+BAAiB,UAAU;AAAA,IACvC;AACA,QAAI,mCAAY,IAAI,KAAK,eAAe;AACtC,YAAM,IAAI,kCAAoB,cAAc;AAAA,IAC9C;AACA,cAAM,oBAAM,cAAc;AAAA,EAC5B;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/request-reply/client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { performance } from 'node:perf_hooks';\nimport
|
|
1
|
+
{"version":3,"sources":["../../src/request-reply/client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { performance } from 'node:perf_hooks';\nimport { NoResponderError, RequestTimeoutError } from './errors';\nimport type { RedisClient } from './redisClient';\nimport type { JSONReply, JSONValue, PublishedRequest, RequestEnvelope } from './types';\nimport { jsonReplySchema } from './types';\nimport { heartbeatKey, replyKey, requestChannel, sleep } from './utils';\n\nexport interface SendRequestOptions {\n /** Max time to wait for a reply after the request is published (ms). */\n replyTimeoutMs: number;\n /** Sleep between poll attempts when the reply is not yet present (ms). */\n pollIntervalMs: number;\n}\n\nconst DEFAULT_REPLY_TIMEOUT_MS = 60_000;\nconst DEFAULT_POLL_INTERVAL_MS = 500;\n\nfunction resolveSendRequestOptions(options: Partial<SendRequestOptions> | undefined): SendRequestOptions {\n return {\n replyTimeoutMs: options?.replyTimeoutMs ?? DEFAULT_REPLY_TIMEOUT_MS,\n pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,\n };\n}\n\nfunction parseReplyPayload(raw: string) {\n const parsed: unknown = JSON.parse(raw);\n return jsonReplySchema.parse(parsed);\n}\n\nasync function getDelReply(redisClient: RedisClient, rKey: string): Promise<JSONReply | null> {\n const raw = await redisClient.getDel(rKey);\n if (raw === null) {\n return null;\n }\n return parseReplyPayload(raw);\n}\n\n/**\n * Publishes the request (JSON) on the worker channel, then polls the reply key with GETDEL\n * (see https://redis.io/docs/latest/commands/getdel/ ) until a result arrives, the executor\n * heartbeat expires, or the wait budget is exceeded.\n */\nexport async function redisRequest<T extends JSONValue>({\n redis: redisClient,\n executorId,\n path,\n request,\n options,\n}: {\n redis: RedisClient;\n executorId: string;\n path: string;\n request: RequestEnvelope<T>;\n options?: Partial<SendRequestOptions> | undefined;\n}): Promise<JSONReply> {\n const { replyTimeoutMs, pollIntervalMs } = resolveSendRequestOptions(options);\n const aliveK = heartbeatKey(executorId);\n if ((await redisClient.exists(aliveK)) === 0) {\n throw new NoResponderError(executorId);\n }\n\n const requestId = randomUUID();\n const rKey = replyKey(requestId);\n const payload: PublishedRequest = {\n replyKey: rKey,\n path,\n body: request.body,\n headers: request.headers,\n };\n const serializedPayload = JSON.stringify(payload);\n const subscriberCount = await redisClient.publish(requestChannel(executorId), serializedPayload);\n if (subscriberCount === 0) {\n throw new NoResponderError(executorId, 'No executor received the published request');\n }\n\n const replyDeadline = performance.now() + replyTimeoutMs;\n // Give the executor a moment to process before the first poll.\n await sleep(Math.min(200, replyTimeoutMs));\n for (;;) {\n const replyPayload = await getDelReply(redisClient, rKey);\n if (replyPayload) {\n return replyPayload;\n }\n // We already checked that the executor was alive before sending, but it may have\n // died while we wait. Recheck here so we fail soon instead of waiting until the timeout.\n if ((await redisClient.exists(aliveK)) === 0) {\n throw new NoResponderError(executorId);\n }\n if (performance.now() >= replyDeadline) {\n throw new RequestTimeoutError(replyTimeoutMs);\n }\n await sleep(pollIntervalMs);\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,kBAAkB,2BAA2B;AAGtD,SAAS,uBAAuB;AAChC,SAAS,cAAc,UAAU,gBAAgB,aAAa;AAS9D,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AAEjC,SAAS,0BAA0B,SAAsE;AACvG,SAAO;AAAA,IACL,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,gBAAgB,SAAS,kBAAkB;AAAA,EAC7C;AACF;AAEA,SAAS,kBAAkB,KAAa;AACtC,QAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,SAAO,gBAAgB,MAAM,MAAM;AACrC;AAEA,eAAe,YAAY,aAA0B,MAAyC;AAC5F,QAAM,MAAM,MAAM,YAAY,OAAO,IAAI;AACzC,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,GAAG;AAC9B;AAOA,eAAsB,aAAkC;AAAA,EACtD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMuB;AACrB,QAAM,EAAE,gBAAgB,eAAe,IAAI,0BAA0B,OAAO;AAC5E,QAAM,SAAS,aAAa,UAAU;AACtC,MAAK,MAAM,YAAY,OAAO,MAAM,MAAO,GAAG;AAC5C,UAAM,IAAI,iBAAiB,UAAU;AAAA,EACvC;AAEA,QAAM,YAAY,WAAW;AAC7B,QAAM,OAAO,SAAS,SAAS;AAC/B,QAAM,UAA4B;AAAA,IAChC,UAAU;AAAA,IACV;AAAA,IACA,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,EACnB;AACA,QAAM,oBAAoB,KAAK,UAAU,OAAO;AAChD,QAAM,kBAAkB,MAAM,YAAY,QAAQ,eAAe,UAAU,GAAG,iBAAiB;AAC/F,MAAI,oBAAoB,GAAG;AACzB,UAAM,IAAI,iBAAiB,YAAY,4CAA4C;AAAA,EACrF;AAEA,QAAM,gBAAgB,YAAY,IAAI,IAAI;AAE1C,QAAM,MAAM,KAAK,IAAI,KAAK,cAAc,CAAC;AACzC,aAAS;AACP,UAAM,eAAe,MAAM,YAAY,aAAa,IAAI;AACxD,QAAI,cAAc;AAChB,aAAO;AAAA,IACT;AAGA,QAAK,MAAM,YAAY,OAAO,MAAM,MAAO,GAAG;AAC5C,YAAM,IAAI,iBAAiB,UAAU;AAAA,IACvC;AACA,QAAI,YAAY,IAAI,KAAK,eAAe;AACtC,YAAM,IAAI,oBAAoB,cAAc;AAAA,IAC9C;AACA,UAAM,MAAM,cAAc;AAAA,EAC5B;AACF;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { RedisClientType } from 'redis';
|
|
2
1
|
import type { Logger } from 'winston';
|
|
2
|
+
import type { RedisClient } from './redisClient';
|
|
3
3
|
import type { RequestHandler } from './types';
|
|
4
4
|
export interface RunExecutorOptions {
|
|
5
5
|
/** How often to refresh the heartbeat key (ms). */
|
|
@@ -46,9 +46,9 @@ export declare class RequestReplyExecutor {
|
|
|
46
46
|
constructor({ executorId, redis, subscriberClient, requestHandler, onError, logger, options, }: {
|
|
47
47
|
executorId: string;
|
|
48
48
|
/** Connected command client, used only for SET (reply + heartbeat). Caller owns its lifecycle. */
|
|
49
|
-
redis:
|
|
49
|
+
redis: RedisClient;
|
|
50
50
|
/** Connected client to SUBSCRIBE on (duplicate or Sentinel). Caller owns its lifecycle. */
|
|
51
|
-
subscriberClient:
|
|
51
|
+
subscriberClient: RedisClient;
|
|
52
52
|
requestHandler: RequestHandler;
|
|
53
53
|
onError?: RequestReplyErrorHandler | undefined;
|
|
54
54
|
logger: Logger;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/request-reply/executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/request-reply/executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAItC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,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,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAc;IAC/C,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,WAAW,CAAC;QACnB,2FAA2F;QAC3F,gBAAgB,EAAE,WAAW,CAAC;QAC9B,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"}
|
|
@@ -101,7 +101,6 @@ var RequestReplyExecutor = class {
|
|
|
101
101
|
...(0, import_errorLogFields.extractErrorLogFields)(err)
|
|
102
102
|
});
|
|
103
103
|
this.onError?.(err, { executorId: this.executorId, channel: this.channel });
|
|
104
|
-
this.stopHeartbeat();
|
|
105
104
|
});
|
|
106
105
|
this.subscriberClient.on("end", () => {
|
|
107
106
|
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":["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;
|
|
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;
|
|
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 @@
|
|
|
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":[]}
|