fix(rectification): 请求内 Case 档案只读投影写即失效缓存
同一请求里 dossier/compute 按 (fn, userId, caseId) 合并重复读,其它 RPC 与 .from 立即失效。路由入口各包一处,零调用点改动。
This commit is contained in:
@@ -30,6 +30,7 @@ import { isProductEnabled } from "@/lib/product-access";
|
||||
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { withRectificationRequestCache } from "@/lib/rectification-agentic/v9/request-cache";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
|
||||
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
|
||||
@@ -150,7 +151,7 @@ export async function POST(request: Request) {
|
||||
let accounting;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
accounting = withRectificationRequestCache(createAdminSupabaseClient());
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/agent");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { withRectificationRequestCache } from "@/lib/rectification-agentic/v9/request-cache";
|
||||
import { isProductEnabled } from "@/lib/product-access";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { RectificationToolServiceError } from "@/lib/rectification-agentic/v9/tool-service";
|
||||
@@ -59,7 +60,7 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
let accounting;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
accounting = withRectificationRequestCache(createAdminSupabaseClient());
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/candidates/accept");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { withRectificationRequestCache } from "@/lib/rectification-agentic/v9/request-cache";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
loadV9CaseDossier,
|
||||
@@ -32,7 +33,7 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
let accounting;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
accounting = withRectificationRequestCache(createAdminSupabaseClient());
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/repair-exit");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { withRectificationRequestCache } from "@/lib/rectification-agentic/v9/request-cache";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
loadV9CaseDossier,
|
||||
@@ -29,7 +30,7 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
let accounting;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
accounting = withRectificationRequestCache(createAdminSupabaseClient());
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/rectification/cases/[caseId]");
|
||||
}
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import { resolveExactSkillPackage } from "@/lib/skill-package-registry";
|
||||
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { withRectificationRequestCache } from "@/lib/rectification-agentic/v9/request-cache";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { getRectificationV9RegenerationAgent } from "@/mastra/agentic-rectification";
|
||||
import { logRectificationDeliveryTurn } from "@/lib/rectification-agentic/v9/delivery-turn-guard";
|
||||
@@ -64,7 +65,7 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
let accounting;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
accounting = withRectificationRequestCache(createAdminSupabaseClient());
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/turns/[turnId]/regenerate");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Request-scoped cache for Case read projections.
|
||||
*
|
||||
* `get_agentic_rectification_case_dossier` and
|
||||
* `get_agentic_rectification_case_compute` are unchanged for the life of a
|
||||
* request until this client performs any other RPC or table access. Call sites
|
||||
* keep calling `loadV9CaseDossier` as before; the wrapper sits on the client
|
||||
* object so direct `accounting.rpc(...)` paths are covered too.
|
||||
*
|
||||
* The cache is a closure over a Map created by each `withRectificationRequestCache`
|
||||
* call. It is not stored on `globalThis` or at module scope.
|
||||
*
|
||||
* Other properties are forwarded through a Proxy so `.from(table).update(...)`
|
||||
* (used via `as never` in a few call sites) still works, and accessing them
|
||||
* invalidates the same way a non-projection RPC does.
|
||||
*/
|
||||
import type { RectificationRpcClient } from "./tool-service";
|
||||
|
||||
export const RECTIFICATION_CACHED_PROJECTION_RPCS = [
|
||||
"get_agentic_rectification_case_dossier",
|
||||
"get_agentic_rectification_case_compute",
|
||||
] as const;
|
||||
|
||||
type CachedProjectionRpc = (typeof RECTIFICATION_CACHED_PROJECTION_RPCS)[number];
|
||||
|
||||
const cachedProjectionRpcSet = new Set<string>(RECTIFICATION_CACHED_PROJECTION_RPCS);
|
||||
|
||||
function isCachedProjectionRpc(fn: string): fn is CachedProjectionRpc {
|
||||
return cachedProjectionRpcSet.has(fn);
|
||||
}
|
||||
|
||||
function projectionCacheKey(fn: string, args: Record<string, unknown>): string | null {
|
||||
const userId = args.p_user_id;
|
||||
const caseId = args.p_case_id;
|
||||
if (typeof userId !== "string" || typeof caseId !== "string" || !userId || !caseId) {
|
||||
return null;
|
||||
}
|
||||
return `${fn}\0${userId}\0${caseId}`;
|
||||
}
|
||||
|
||||
type RpcResult = Awaited<ReturnType<RectificationRpcClient["rpc"]>>;
|
||||
|
||||
export function withRectificationRequestCache<T extends RectificationRpcClient>(accounting: T): T {
|
||||
const cache = new Map<string, PromiseLike<RpcResult>>();
|
||||
const originalRpc = accounting.rpc.bind(accounting);
|
||||
|
||||
function invalidate(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
function cachedRpc(fn: string, args: Record<string, unknown>): PromiseLike<RpcResult> {
|
||||
if (!isCachedProjectionRpc(fn)) {
|
||||
invalidate();
|
||||
return originalRpc(fn, args);
|
||||
}
|
||||
const key = projectionCacheKey(fn, args);
|
||||
if (!key) return originalRpc(fn, args);
|
||||
const hit = cache.get(key);
|
||||
if (hit) return hit;
|
||||
const pending = originalRpc(fn, args);
|
||||
cache.set(key, pending);
|
||||
return pending;
|
||||
}
|
||||
|
||||
return new Proxy(accounting, {
|
||||
get(target, property) {
|
||||
if (property === "rpc") return cachedRpc;
|
||||
invalidate();
|
||||
const value = Reflect.get(target, property);
|
||||
if (typeof value === "function") {
|
||||
return value.bind(target);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
}) as T;
|
||||
}
|
||||
Reference in New Issue
Block a user