// --------------------------------------------------------------------------- // Invoke-path batching: DataLoader semantics via Effect Request/RequestResolver. // // The invoke hot path loads four things per call: the tool row, the active // policy rule set, the connection row, and the integration row. Called once, // that's four point queries, which is fine. Called N times concurrently (code // mode fanning out `await Promise.all(items.map(x => tools.foo.bar(x)))`, an // MCP host serving parallel tool calls), it's 4×N queries against the same // handful of rows: the classic N+1. // // Each lookup below is an Effect `Request` plus a `RequestResolver`. The // Effect runtime collects every request issued within the same microtask // window (resolver delay is `Effect.yieldNow`) and hands the resolver the // whole batch, which it serves with ONE query per table. Sequential callers // pay nothing: a batch of one is exactly the point query this replaced. There // is no cross-batch caching; every batch re-reads storage, so invalidation // semantics are unchanged. // // Requests are keyed by plain data (owner/slug/name strings), never rows, so // equality is meaningful and nothing heavy is retained. // --------------------------------------------------------------------------- import { Effect, Exit, Request, RequestResolver } from "effect"; import type { ConnectionRow, IntegrationRow, ToolInvocationRow } from "./core-schema"; import { activeFumaDbRef, type StorageFailure } from "./fuma-runtime"; import type { ConnectionName, IntegrationSlug, Owner, ToolName } from "./ids"; // --------------------------------------------------------------------------- // Request types // --------------------------------------------------------------------------- interface GetToolRow extends Request.Request { readonly _tag: "GetToolRow"; readonly owner: Owner; readonly integration: IntegrationSlug; readonly connection: ConnectionName; readonly tool: ToolName; } const GetToolRow = Request.tagged("GetToolRow"); interface GetConnectionRow extends Request.Request { readonly _tag: "GetConnectionRow"; readonly owner: Owner; readonly integration: IntegrationSlug; readonly name: ConnectionName; } const GetConnectionRow = Request.tagged("GetConnectionRow"); interface GetIntegrationRow extends Request.Request { readonly _tag: "GetIntegrationRow"; readonly slug: IntegrationSlug; } const GetIntegrationRow = Request.tagged("GetIntegrationRow"); // The policy rule set is one shared snapshot per batch window: every request // is identical, so the resolver runs the underlying load once and fans the // result out to all awaiting fibers. interface GetPolicyRuleSet extends Request.Request { readonly _tag: "GetPolicyRuleSet"; } // --------------------------------------------------------------------------- // Wiring: the executor passes its storage closures in; we hand back // point-lookup functions with identical signatures to the ones they replace. // --------------------------------------------------------------------------- export interface InvokeBatchingDeps { /** Batched tool-row load: one query for all (owner, integration, connection, * tool) tuples in the window, projected to the invocation columns. */ readonly loadToolRows: ( requests: readonly { readonly owner: Owner; readonly integration: IntegrationSlug; readonly connection: ConnectionName; readonly tool: ToolName; }[], ) => Effect.Effect; readonly loadConnectionRows: ( requests: readonly { readonly owner: Owner; readonly integration: IntegrationSlug; readonly name: ConnectionName; }[], ) => Effect.Effect; readonly loadIntegrationRows: ( slugs: readonly IntegrationSlug[], ) => Effect.Effect; readonly loadPolicyRuleSet: () => Effect.Effect; } export interface InvokeBatching { readonly getToolRow: (input: { readonly owner: Owner; readonly integration: IntegrationSlug; readonly connection: ConnectionName; readonly tool: ToolName; }) => Effect.Effect; readonly getConnectionRow: (input: { readonly owner: Owner; readonly integration: IntegrationSlug; readonly name: ConnectionName; }) => Effect.Effect; readonly getIntegrationRow: ( slug: IntegrationSlug, ) => Effect.Effect; readonly getPolicyRuleSet: () => Effect.Effect; } // Batches from different fibers execute the `runAll` effect on a fiber forked // with the FIRST caller's context. Inside `fuma.transaction` the storage // handle rides on `activeFumaDbRef` in fiber context, so joining a shared // batch would read through the wrong (or no) transaction. Transactional // callers bypass the batch window and run the load directly on their own // fiber — same query, exact transaction semantics, no cross-fiber sharing. const inTransaction: Effect.Effect = Effect.map( Effect.service(activeFumaDbRef), (active) => active !== null, ); /** Complete every entry in a batch from a keyed index of loaded rows. A * request whose key has no row completes with `null`: absence is a value * (the invoke path turns it into its own typed not-found error). A load * failure fails every entry in the batch, mirroring what each point query * would have done. */ const completeFromIndex = >( entries: readonly Request.Entry[], load: Effect.Effect>, StorageFailure>, keyOf: (request: A) => string, ): Effect.Effect => load.pipe( Effect.matchEffect({ onSuccess: (index) => Effect.sync(() => { for (const entry of entries) { entry.completeUnsafe( Exit.succeed((index.get(keyOf(entry.request)) ?? null) as Request.Success), ); } }), onFailure: (error) => Effect.sync(() => { for (const entry of entries) { // The constraint pins A's error channel to StorageFailure, but the // conditional `Request.Error` doesn't reduce over an unresolved // generic — assert the equivalence once here. entry.completeUnsafe(Exit.fail(error) as Exit.Exit>); } }), }), ); /** The runtime collects entries per window without deduping equal requests; * two fibers asking for the same connection contribute two entries. Loaders * receive each distinct key once (DataLoader's contract) and both entries * complete from the shared index. */ const uniqueBy = (items: readonly T[], key: (item: T) => string): readonly T[] => { const seen = new Set(); const out: T[] = []; for (const item of items) { const k = key(item); if (seen.has(k)) continue; seen.add(k); out.push(item); } return out; }; const toolKey = (r: { readonly owner: string; readonly integration: string; readonly connection: string; readonly tool: string; }): string => `${r.owner} ${r.integration} ${r.connection} ${r.tool}`; const connectionKey = (r: { readonly owner: string; readonly integration: string; readonly name: string; }): string => `${r.owner} ${r.integration} ${r.name}`; export const makeInvokeBatching = ( deps: InvokeBatchingDeps, ): InvokeBatching => { const toolResolver = RequestResolver.make((entries) => completeFromIndex( entries, deps .loadToolRows( uniqueBy( entries.map((entry) => entry.request), toolKey, ), ) .pipe( Effect.map( (rows) => new Map( rows.map((row) => [ toolKey({ owner: row.owner, integration: row.integration, connection: row.connection, tool: row.name, }), row, ]), ), ), ), toolKey, ), ); const connectionResolver = RequestResolver.make((entries) => completeFromIndex( entries, deps .loadConnectionRows( uniqueBy( entries.map((entry) => entry.request), connectionKey, ), ) .pipe( Effect.map( (rows) => new Map( rows.map((row) => [ connectionKey({ owner: row.owner, integration: row.integration, name: row.name, }), row, ]), ), ), ), connectionKey, ), ); const integrationResolver = RequestResolver.make((entries) => completeFromIndex( entries, deps .loadIntegrationRows( uniqueBy( entries.map((entry) => entry.request.slug), String, ), ) .pipe(Effect.map((rows) => new Map(rows.map((row) => [String(row.slug), row])))), (request) => String(request.slug), ), ); const policyResolver = RequestResolver.make>((entries) => deps.loadPolicyRuleSet().pipe( Effect.matchEffect({ onSuccess: (ruleSet) => Effect.sync(() => { for (const entry of entries) entry.completeUnsafe(Exit.succeed(ruleSet)); }), onFailure: (error) => Effect.sync(() => { for (const entry of entries) entry.completeUnsafe(Exit.fail(error)); }), }), ), ); const policyRequest = Request.of>()({ _tag: "GetPolicyRuleSet", }); const first = (rows: readonly A[]): A | null => rows[0] ?? null; return { getToolRow: (input) => Effect.flatMap(inTransaction, (transactional) => transactional ? Effect.map(deps.loadToolRows([input]), first) : Effect.request(GetToolRow(input), toolResolver), ), getConnectionRow: (input) => Effect.flatMap(inTransaction, (transactional) => transactional ? Effect.map(deps.loadConnectionRows([input]), first) : Effect.request(GetConnectionRow(input), connectionResolver), ), getIntegrationRow: (slug) => Effect.flatMap(inTransaction, (transactional) => transactional ? Effect.map(deps.loadIntegrationRows([slug]), first) : Effect.request(GetIntegrationRow({ slug }), integrationResolver), ), getPolicyRuleSet: () => Effect.flatMap(inTransaction, (transactional) => transactional ? deps.loadPolicyRuleSet() : Effect.request(policyRequest, policyResolver), ), }; };