fix(metadata): await object cache invalidation on connection refresh
This commit is contained in:
1 parent
f95650f039
commit
faf259fcc2
17 files changed
+536
-32
No files matched your search
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("@/lib/backend/api", () => mocks);
|
||||
|
||||
import { cancelObjectDdlLoadsForConnection, getObjectDdlCacheDebugStateForTests, invalidateObjectDdl, invalidateObjectDdlCache, loadObjectDdl, objectDdlCacheKey } from "@/lib/metadata/objectDdlCache";
|
||||
import { isObjectCacheInvalidationError } from "@/lib/metadata/objectCacheInvalidationError";
|
||||
import { clearMetadataRuntimeCache } from "@/lib/metadata/metadataRuntimeCache";
|
||||
|
||||
const request = { connectionId: "c1", database: "app", schema: "public", tableName: "users", catalog: "analytics" } as const;
|
||||
@@ -263,4 +264,156 @@ describe("objectDdlCache", () => {
|
||||
expect(mocks.getTableDisplayDdl).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.deleteSchemaCachePrefix).not.toHaveBeenCalledWith("object-ddl:v1:c1:");
|
||||
});
|
||||
|
||||
describe("strict connection-level invalidation", () => {
|
||||
const connectionMatch = { connectionId: "c1" };
|
||||
|
||||
it.each(["object-ddl:v1:c1:", "object-meta:v1:c1:"])("waits for the other namespace when %s fails", async (failedPrefix) => {
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
mocks.deleteSchemaCachePrefix.mockImplementation((prefix: string) => (prefix === failedPrefix ? Promise.reject(new Error("locked")) : pending));
|
||||
let settled = false;
|
||||
const outcome = invalidateObjectDdlCache(connectionMatch, { strict: true }).then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
(error: unknown) => {
|
||||
settled = true;
|
||||
return error;
|
||||
},
|
||||
);
|
||||
await vi.waitFor(() => expect(mocks.deleteSchemaCachePrefix).toHaveBeenCalledTimes(2));
|
||||
// Drain the rejection handlers, without releasing the other deletion.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(settled).toBe(false);
|
||||
release();
|
||||
const failure = await outcome;
|
||||
expect(isObjectCacheInvalidationError(failure)).toBe(true);
|
||||
expect(failure).toMatchObject({ scope: failedPrefix, message: "locked" });
|
||||
});
|
||||
|
||||
function objectPrefixes(): string[] {
|
||||
return mocks.deleteSchemaCachePrefix.mock.calls.map(([prefix]) => prefix as string);
|
||||
}
|
||||
|
||||
function rejectAllDeletions(error: Error) {
|
||||
let rejectGate: (reason: Error) => void = () => {};
|
||||
const gate = new Promise<void>((_resolve, reject) => {
|
||||
rejectGate = reject;
|
||||
});
|
||||
mocks.deleteSchemaCachePrefix.mockImplementation(() =>
|
||||
gate.then(
|
||||
() => undefined,
|
||||
() => {
|
||||
throw error;
|
||||
},
|
||||
),
|
||||
);
|
||||
return () => rejectGate(error);
|
||||
}
|
||||
|
||||
it("rejects with a marked object cache error when the persisted deletion fails", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("sqlite locked"));
|
||||
|
||||
const failure = await invalidateObjectDdlCache(connectionMatch, { strict: true }).then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(failure).toBeInstanceOf(Error);
|
||||
expect(isObjectCacheInvalidationError(failure)).toBe(true);
|
||||
expect((failure as Error).message).toBe("sqlite locked");
|
||||
// Both the DDL and the object-metadata namespace deletions were attempted.
|
||||
expect(objectPrefixes()).toContain("object-ddl:v1:c1:");
|
||||
expect(objectPrefixes()).toContain("object-meta:v1:c1:");
|
||||
});
|
||||
|
||||
it("keeps default invalidation best-effort when the persisted deletion fails", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("sqlite locked"));
|
||||
|
||||
await expect(invalidateObjectDdlCache(connectionMatch)).resolves.toBeUndefined();
|
||||
expect(objectPrefixes()).toEqual(["object-ddl:v1:c1:", "object-meta:v1:c1:"]);
|
||||
});
|
||||
|
||||
it("still rejects in strict mode when a deletion rejects without a reason", async () => {
|
||||
// A bare Promise.reject() has reason undefined; settled status, not the
|
||||
// reason's value, must decide failure.
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(undefined as never);
|
||||
|
||||
const failure = await invalidateObjectDdlCache(connectionMatch, { strict: true }).then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(isObjectCacheInvalidationError(failure)).toBe(true);
|
||||
});
|
||||
|
||||
it("lets a strict call sharing a pending best-effort deletion observe the failure", async () => {
|
||||
const failAll = rejectAllDeletions(new Error("disk full"));
|
||||
|
||||
const bestEffort = invalidateObjectDdlCache(connectionMatch);
|
||||
const strict = invalidateObjectDdlCache(connectionMatch, { strict: true });
|
||||
// The strict call must reuse the in-flight deletions instead of starting new ones.
|
||||
await vi.waitFor(() => expect(objectPrefixes()).toEqual(["object-ddl:v1:c1:", "object-meta:v1:c1:"]));
|
||||
|
||||
failAll();
|
||||
await expect(bestEffort).resolves.toBeUndefined();
|
||||
const failure = await strict.then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(isObjectCacheInvalidationError(failure)).toBe(true);
|
||||
expect((failure as Error).message).toBe("disk full");
|
||||
});
|
||||
|
||||
it("retries the persisted deletion on the next strict invalidation after a failure", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValueOnce(new Error("locked")).mockRejectedValueOnce(new Error("locked"));
|
||||
|
||||
await expect(invalidateObjectDdlCache(connectionMatch, { strict: true })).rejects.toThrow("locked");
|
||||
|
||||
await expect(invalidateObjectDdlCache(connectionMatch, { strict: true })).resolves.toBeUndefined();
|
||||
const prefixes = objectPrefixes();
|
||||
expect(prefixes.filter((prefix) => prefix === "object-ddl:v1:c1:")).toHaveLength(2);
|
||||
expect(prefixes.filter((prefix) => prefix === "object-meta:v1:c1:")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps non-force reads working when a concurrent deletion fails", async () => {
|
||||
const failAll = rejectAllDeletions(new Error("locked"));
|
||||
mocks.getTableDisplayDdl.mockResolvedValue("fresh ddl");
|
||||
|
||||
const invalidation = invalidateObjectDdlCache({ connectionId: request.connectionId, database: request.database, schema: request.schema, tableName: request.tableName });
|
||||
const read = loadObjectDdl(request);
|
||||
failAll();
|
||||
await expect(read).resolves.toEqual({ ddl: "fresh ddl", cacheStatus: "remote" });
|
||||
await expect(invalidation).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("force refresh proceeds when the persisted deletion fails", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("locked"));
|
||||
mocks.getTableDisplayDdl.mockResolvedValue("new ddl");
|
||||
|
||||
await expect(loadObjectDdl(request, { force: true })).resolves.toEqual({ ddl: "new ddl", cacheStatus: "remote" });
|
||||
});
|
||||
|
||||
it("keeps object-level invalidation best-effort when the deletion fails", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("locked"));
|
||||
|
||||
await expect(invalidateObjectDdl(request)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("encodes connection ids and isolates similar ids in persisted deletions", async () => {
|
||||
mocks.persisted.set("object-ddl:v1:conn10:db:public:t::TABLE:", { version: 1, cachedAt: new Date().toISOString(), ddl: "conn10 ddl" });
|
||||
|
||||
await invalidateObjectDdlCache({ connectionId: "conn1" });
|
||||
|
||||
expect(objectPrefixes()).toContain("object-ddl:v1:conn1:");
|
||||
expect(objectPrefixes()).toContain("object-meta:v1:conn1:");
|
||||
expect([...mocks.persisted.keys()]).toContain("object-ddl:v1:conn10:db:public:t::TABLE:");
|
||||
|
||||
await invalidateObjectDdlCache({ connectionId: "a:b c%中" });
|
||||
expect(mocks.deleteSchemaCachePrefix).toHaveBeenCalledWith("object-ddl:v1:a%3Ab%20c%25%E4%B8%AD:");
|
||||
expect(mocks.deleteSchemaCachePrefix).toHaveBeenCalledWith("object-meta:v1:a%3Ab%20c%25%E4%B8%AD:");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("@/lib/backend/api", () => mocks);
|
||||
|
||||
import { cancelObjectMetadataLoadsForConnection, getObjectMetadataCacheDebugStateForTests, invalidateObjectMetadataCache, loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
|
||||
import { isObjectCacheInvalidationError } from "@/lib/metadata/objectCacheInvalidationError";
|
||||
import { clearMetadataRuntimeCache } from "@/lib/metadata/metadataRuntimeCache";
|
||||
|
||||
const request = { connectionId: "c1", database: "app", schema: "public", tableName: "users", catalog: "analytics" } as const;
|
||||
@@ -252,4 +253,32 @@ describe("objectMetadataCache", () => {
|
||||
expect(loader).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.deleteSchemaCachePrefix).not.toHaveBeenCalledWith("object-meta:v1:c1:");
|
||||
});
|
||||
|
||||
describe("strict invalidation", () => {
|
||||
it("rejects with a marked object cache error in strict mode and stays best-effort by default", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("sqlite locked"));
|
||||
|
||||
const failure = await invalidateObjectMetadataCache({ connectionId: "c1" }, { strict: true }).then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(failure).toBeInstanceOf(Error);
|
||||
expect(isObjectCacheInvalidationError(failure)).toBe(true);
|
||||
expect((failure as Error).message).toBe("sqlite locked");
|
||||
expect(mocks.deleteSchemaCachePrefix).toHaveBeenCalledWith("object-meta:v1:c1:");
|
||||
|
||||
mocks.deleteSchemaCachePrefix.mockClear();
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("sqlite locked"));
|
||||
await expect(invalidateObjectMetadataCache({ connectionId: "c1" })).resolves.toBeUndefined();
|
||||
expect(mocks.deleteSchemaCachePrefix).toHaveBeenCalledWith("object-meta:v1:c1:");
|
||||
});
|
||||
|
||||
it("force facet loads tolerate a failed persisted deletion", async () => {
|
||||
mocks.deleteSchemaCachePrefix.mockRejectedValue(new Error("locked"));
|
||||
const loader = vi.fn().mockResolvedValue(["fresh"]);
|
||||
|
||||
await expect(loadObjectMetadataFacet(request, "columns", loader, { force: true })).resolves.toEqual({ value: ["fresh"], cacheStatus: "remote" });
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Error marker for strict object-cache invalidation failures.
|
||||
*
|
||||
* Best-effort invalidation callers keep swallowing persistence errors, so the
|
||||
* only signal a strict caller (connection-node refresh) can rely on is an
|
||||
* explicitly marked error. Detection is name-based rather than instanceof so
|
||||
* duplicated module instances (tests reset modules) still match.
|
||||
*/
|
||||
|
||||
export interface ObjectCacheInvalidationOptions {
|
||||
/** Propagate persistence deletion failures instead of swallowing them. */
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
const OBJECT_CACHE_INVALIDATION_ERROR_NAME = "ObjectCacheInvalidationError";
|
||||
|
||||
export class ObjectCacheInvalidationError extends Error {
|
||||
readonly code = "OBJECT_CACHE_INVALIDATION_FAILED";
|
||||
/** Cache prefix whose persisted deletion failed. */
|
||||
readonly scope: string;
|
||||
/** Original error thrown by the deletion. */
|
||||
readonly reason: unknown;
|
||||
|
||||
constructor(message: string, scope: string, reason?: unknown) {
|
||||
super(message);
|
||||
this.name = OBJECT_CACHE_INVALIDATION_ERROR_NAME;
|
||||
this.scope = scope;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
export function isObjectCacheInvalidationError(value: unknown): value is ObjectCacheInvalidationError {
|
||||
return value instanceof Error && value.name === OBJECT_CACHE_INVALIDATION_ERROR_NAME;
|
||||
}
|
||||
|
||||
/** Wrap a deletion failure, passing through an already-marked error unchanged. */
|
||||
export function toObjectCacheInvalidationError(error: unknown, scope: string): ObjectCacheInvalidationError {
|
||||
if (isObjectCacheInvalidationError(error)) return error;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new ObjectCacheInvalidationError(message, scope, error);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ActiveCacheReadTracker } from "./activeCacheReadTracker";
|
||||
import type { ObjectSourceKind } from "@/types/database";
|
||||
import type { MetadataCacheInvalidation } from "./metadataResultCache";
|
||||
import { invalidateObjectMetadataCache } from "./objectMetadataCache";
|
||||
import { toObjectCacheInvalidationError, type ObjectCacheInvalidationOptions } from "./objectCacheInvalidationError";
|
||||
import { getMetadataRuntimeCache, invalidateMetadataRuntimeCachePrefix, recordMetadataCacheL2Hit, recordMetadataCacheRemoteMiss, setMetadataRuntimeCache } from "./metadataRuntimeCache";
|
||||
|
||||
const OBJECT_DDL_CACHE_PREFIX = "object-ddl:v1";
|
||||
@@ -56,14 +57,6 @@ async function saveSchemaCacheSafe(cacheKey: string, payload: unknown): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSchemaCachePrefixSafe(prefix: string): Promise<void> {
|
||||
try {
|
||||
await api.deleteSchemaCachePrefix(prefix);
|
||||
} catch {
|
||||
// Cache invalidation is best effort when running with a reduced backend.
|
||||
}
|
||||
}
|
||||
|
||||
function cacheSegment(value: string | undefined): string {
|
||||
return encodeURIComponent(value ?? "");
|
||||
}
|
||||
@@ -94,7 +87,9 @@ function decodeCachedDdl(payload: unknown): string | null {
|
||||
}
|
||||
|
||||
async function waitForPendingInvalidations(cacheKey: string): Promise<void> {
|
||||
const pending = [...pendingInvalidations.entries()].filter(([prefix]) => cacheKey.startsWith(prefix)).map(([, promise]) => promise);
|
||||
// Readers only wait for pending deletions to settle; the failure itself is
|
||||
// reported by the caller that started the invalidation.
|
||||
const pending = [...pendingInvalidations.entries()].filter(([prefix]) => cacheKey.startsWith(prefix)).map(([, promise]) => promise.catch(() => undefined));
|
||||
if (pending.length) await Promise.all(pending);
|
||||
}
|
||||
|
||||
@@ -129,7 +124,9 @@ function invalidatePersistedPrefix(prefix: string): Promise<void> {
|
||||
if (existing) return existing;
|
||||
const deletion = (async () => {
|
||||
await waitForPendingWrites(prefix);
|
||||
await deleteSchemaCachePrefixSafe(prefix);
|
||||
// Deletion failures propagate so strict callers can observe them;
|
||||
// best-effort callers catch at their own boundary.
|
||||
await api.deleteSchemaCachePrefix(prefix);
|
||||
})().finally(() => {
|
||||
if (pendingInvalidations.get(prefix) === deletion) pendingInvalidations.delete(prefix);
|
||||
});
|
||||
@@ -174,7 +171,8 @@ export async function loadObjectDdl(request: ObjectDdlRequest, options?: { force
|
||||
invalidateMetadataRuntimeCachePrefix(cacheKey);
|
||||
const priorInvalidations = waitForPendingInvalidations(cacheKey);
|
||||
const deletion = invalidatePersistedPrefix(cacheKey);
|
||||
await Promise.all([priorInvalidations, deletion]);
|
||||
// A failed prefix deletion must not fail the force load itself.
|
||||
await Promise.allSettled([priorInvalidations, deletion]);
|
||||
} else {
|
||||
await waitForPendingInvalidations(cacheKey);
|
||||
}
|
||||
@@ -203,14 +201,22 @@ export async function loadObjectDdl(request: ObjectDdlRequest, options?: { force
|
||||
return { ddl: await loadRemoteDdl(request, cacheKey, options?.force === true), cacheStatus: "remote" };
|
||||
}
|
||||
|
||||
export async function invalidateObjectDdlCache(match: MetadataCacheInvalidation): Promise<void> {
|
||||
export async function invalidateObjectDdlCache(match: MetadataCacheInvalidation, options?: ObjectCacheInvalidationOptions): Promise<void> {
|
||||
const prefix = invalidationPrefix(match);
|
||||
activeCacheReads.invalidatePrefix(prefix);
|
||||
for (const cacheKey of [...remoteLoads.keys()]) if (cacheKey.startsWith(prefix)) invalidateRemoteLoad(cacheKey);
|
||||
invalidateMetadataRuntimeCachePrefix(prefix);
|
||||
|
||||
const deletion = invalidatePersistedPrefix(prefix);
|
||||
await Promise.all([deletion, invalidateObjectMetadataCache(match)]);
|
||||
const metadataInvalidation = invalidateObjectMetadataCache(match, options);
|
||||
// Wait for both namespace deletions to settle before deciding, so a strict
|
||||
// caller never reports failure while the other deletion is still in flight.
|
||||
const [ddlSettled, metaSettled] = await Promise.allSettled([deletion, metadataInvalidation]);
|
||||
if (!options?.strict) return;
|
||||
// Judge by settled status, never by the rejection reason's value: a bare
|
||||
// Promise.reject() has reason undefined and must still count as a failure.
|
||||
if (ddlSettled.status === "rejected") throw toObjectCacheInvalidationError(ddlSettled.reason, prefix);
|
||||
if (metaSettled.status === "rejected") throw toObjectCacheInvalidationError(metaSettled.reason, prefix);
|
||||
}
|
||||
|
||||
export async function invalidateObjectDdl(request: ObjectDdlRequest): Promise<void> {
|
||||
@@ -218,7 +224,7 @@ export async function invalidateObjectDdl(request: ObjectDdlRequest): Promise<vo
|
||||
activeCacheReads.invalidatePrefix(cacheKey);
|
||||
invalidateRemoteLoad(cacheKey);
|
||||
invalidateMetadataRuntimeCachePrefix(cacheKey);
|
||||
await Promise.all([
|
||||
await Promise.allSettled([
|
||||
invalidatePersistedPrefix(cacheKey),
|
||||
invalidateObjectMetadataCache({
|
||||
connectionId: request.connectionId,
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as api from "@/lib/backend/api";
|
||||
import { ActiveCacheReadTracker } from "./activeCacheReadTracker";
|
||||
import type { MetadataCacheInvalidation } from "./metadataResultCache";
|
||||
import type { ObjectDdlRequest } from "./objectDdlCache";
|
||||
import { toObjectCacheInvalidationError, type ObjectCacheInvalidationOptions } from "./objectCacheInvalidationError";
|
||||
import { getMetadataRuntimeCache, invalidateMetadataRuntimeCachePrefix, recordMetadataCacheL2Hit, recordMetadataCacheRemoteMiss, setMetadataRuntimeCache } from "./metadataRuntimeCache";
|
||||
|
||||
const OBJECT_METADATA_CACHE_PREFIX = "object-meta:v1";
|
||||
@@ -41,14 +42,6 @@ async function saveSchemaCacheSafe(cacheKey: string, payload: unknown): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSchemaCachePrefixSafe(prefix: string): Promise<void> {
|
||||
try {
|
||||
await api.deleteSchemaCachePrefix(prefix);
|
||||
} catch {
|
||||
// Cache invalidation is best effort when running with a reduced backend.
|
||||
}
|
||||
}
|
||||
|
||||
export type ObjectMetadataFacet = "columns" | "indexes" | "foreign-keys" | "constraints" | "triggers" | "comment" | "owner";
|
||||
|
||||
function cacheSegment(value: string | undefined): string {
|
||||
@@ -81,7 +74,9 @@ function decodeEnvelope<T>(payload: unknown): { hit: true; value: T } | undefine
|
||||
}
|
||||
|
||||
async function waitForPendingInvalidations(cacheKey: string): Promise<void> {
|
||||
const pending = [...pendingInvalidations.entries()].filter(([prefix]) => cacheKey.startsWith(prefix)).map(([, promise]) => promise);
|
||||
// Readers only wait for pending deletions to settle; the failure itself is
|
||||
// reported by the caller that started the invalidation.
|
||||
const pending = [...pendingInvalidations.entries()].filter(([prefix]) => cacheKey.startsWith(prefix)).map(([, promise]) => promise.catch(() => undefined));
|
||||
if (pending.length) await Promise.all(pending);
|
||||
}
|
||||
|
||||
@@ -116,7 +111,9 @@ function invalidatePersistedPrefix(prefix: string): Promise<void> {
|
||||
if (existing) return existing;
|
||||
const deletion = (async () => {
|
||||
await waitForPendingWrites(prefix);
|
||||
await deleteSchemaCachePrefixSafe(prefix);
|
||||
// Deletion failures propagate so strict callers can observe them;
|
||||
// best-effort callers catch at their own boundary.
|
||||
await api.deleteSchemaCachePrefix(prefix);
|
||||
})().finally(() => {
|
||||
if (pendingInvalidations.get(prefix) === deletion) pendingInvalidations.delete(prefix);
|
||||
});
|
||||
@@ -134,7 +131,8 @@ export async function loadObjectMetadataFacet<T>(request: ObjectDdlRequest, face
|
||||
invalidateMetadataRuntimeCachePrefix(cacheKey);
|
||||
const priorInvalidations = waitForPendingInvalidations(cacheKey);
|
||||
const deletion = invalidatePersistedPrefix(cacheKey);
|
||||
await Promise.all([priorInvalidations, deletion]);
|
||||
// A failed prefix deletion must not fail the force load itself.
|
||||
await Promise.allSettled([priorInvalidations, deletion]);
|
||||
} else {
|
||||
await waitForPendingInvalidations(cacheKey);
|
||||
}
|
||||
@@ -188,13 +186,22 @@ export async function loadObjectMetadataFacet<T>(request: ObjectDdlRequest, face
|
||||
return { value: await entry.promise, cacheStatus: "remote" };
|
||||
}
|
||||
|
||||
export async function invalidateObjectMetadataCache(match: MetadataCacheInvalidation): Promise<void> {
|
||||
export async function invalidateObjectMetadataCache(match: MetadataCacheInvalidation, options?: ObjectCacheInvalidationOptions): Promise<void> {
|
||||
const prefix = invalidationPrefix(match);
|
||||
activeCacheReads.invalidatePrefix(prefix);
|
||||
for (const cacheKey of [...inFlightLoads.keys()]) if (cacheKey.startsWith(prefix)) invalidateInFlightLoad(cacheKey);
|
||||
invalidateMetadataRuntimeCachePrefix(prefix);
|
||||
|
||||
return invalidatePersistedPrefix(prefix);
|
||||
const deletion = invalidatePersistedPrefix(prefix);
|
||||
if (!options?.strict) {
|
||||
await deletion.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deletion;
|
||||
} catch (error) {
|
||||
throw toObjectCacheInvalidationError(error, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/** Invalidate active loads for a disconnected connection without deleting its persisted snapshot. */
|
||||
|
||||
Reference in new issue
Block a user