/** * Cross-context cache invalidation is a best-effort hint protocol. The wire * event intentionally carries neither cached data nor a concrete query key. * Receivers resolve the allowlisted topic through their local policy. */ export const CACHE_INVALIDATION_PROTOCOL_VERSION = 1 as const; export const CACHE_INVALIDATION_WIRE_LIMITS = Object.freeze({ maxWireBytes: 2_048, maxOpaqueIdentifierLength: 128, maxTopicLength: 64, maxEventTtlMs: 5 * 60 * 1_000, maxFutureClockSkewMs: 30_000, }); export type CacheInvalidationTopicDefinition = Readonly<{ topic: string; topicVersion: number; }>; export type CacheInvalidationWireEvent = Readonly<{ protocolVersion: typeof CACHE_INVALIDATION_PROTOCOL_VERSION; eventId: string; sourceId: string; sourceEpoch: string; sequence: number; cacheEpoch: string; topic: string; topicVersion: number; emittedAt: number; expiresAt: number; }>; export type CacheInvalidationParseFailureReason = | "CACHE_EPOCH_MISMATCH" | "EXPIRED" | "INVALID_ENVELOPE" | "MALFORMED_JSON" | "OVERSIZED" | "PROTOCOL_MISMATCH" | "TOPIC_REJECTED"; export type CacheInvalidationParseResult = | Readonly<{ ok: true; value: CacheInvalidationWireEvent }> | Readonly<{ ok: false; reason: CacheInvalidationParseFailureReason; }>; export type CacheInvalidationParsePolicy = Readonly<{ cacheEpoch: string; topicVersions: Readonly>; nowEpochMilliseconds: number; }>; const WIRE_KEYS = Object.freeze([ "cacheEpoch", "emittedAt", "eventId", "expiresAt", "protocolVersion", "sequence", "sourceEpoch", "sourceId", "topic", "topicVersion", ] as const); const OPAQUE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u; const TOPIC = /^[a-z][a-z0-9.-]*$/u; export function isCacheInvalidationOpaqueIdentifier( value: unknown, ): value is string { return ( typeof value === "string" && value.length >= 1 && value.length <= CACHE_INVALIDATION_WIRE_LIMITS.maxOpaqueIdentifierLength && OPAQUE_IDENTIFIER.test(value) ); } export function isCacheInvalidationTopic( value: unknown, ): value is string { return ( typeof value === "string" && value.length >= 1 && value.length <= CACHE_INVALIDATION_WIRE_LIMITS.maxTopicLength && TOPIC.test(value) ); } export function cacheInvalidationWireByteLength(value: string): number { return new TextEncoder().encode(value).byteLength; } export function decodeCacheInvalidationWireEvent( raw: string, policy: CacheInvalidationParsePolicy, ): CacheInvalidationParseResult { if ( typeof raw !== "string" || raw.length > CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes || cacheInvalidationWireByteLength(raw) > CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes ) { return failure("OVERSIZED"); } let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return failure("MALFORMED_JSON"); } return parseCacheInvalidationWireEvent(parsed, policy); } export function parseCacheInvalidationWireEvent( input: unknown, policy: CacheInvalidationParsePolicy, ): CacheInvalidationParseResult { try { return parseCacheInvalidationWireEventUnsafe(input, policy); } catch { return failure("INVALID_ENVELOPE"); } } function parseCacheInvalidationWireEventUnsafe( input: unknown, policy: CacheInvalidationParsePolicy, ): CacheInvalidationParseResult { if (!isExactWireRecord(input)) { return failure("INVALID_ENVELOPE"); } let serialized: string; try { serialized = JSON.stringify(input); } catch { return failure("INVALID_ENVELOPE"); } if ( cacheInvalidationWireByteLength(serialized) > CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes ) { return failure("OVERSIZED"); } if (input.protocolVersion !== CACHE_INVALIDATION_PROTOCOL_VERSION) { return failure("PROTOCOL_MISMATCH"); } if ( !isCacheInvalidationOpaqueIdentifier(input.eventId) || !isCacheInvalidationOpaqueIdentifier(input.sourceId) || !isCacheInvalidationOpaqueIdentifier(input.sourceEpoch) || !isCacheInvalidationOpaqueIdentifier(input.cacheEpoch) || !isCacheInvalidationTopic(input.topic) || !isPositiveSafeInteger(input.sequence) || !isPositiveSafeInteger(input.topicVersion) || !isEpochMilliseconds(input.emittedAt) || !isEpochMilliseconds(input.expiresAt) || input.expiresAt <= input.emittedAt || input.expiresAt - input.emittedAt > CACHE_INVALIDATION_WIRE_LIMITS.maxEventTtlMs || !isEpochMilliseconds(policy.nowEpochMilliseconds) ) { return failure("INVALID_ENVELOPE"); } if (input.cacheEpoch !== policy.cacheEpoch) { return failure("CACHE_EPOCH_MISMATCH"); } if ( !Object.hasOwn(policy.topicVersions, input.topic) || policy.topicVersions[input.topic] !== input.topicVersion ) { return failure("TOPIC_REJECTED"); } if ( input.expiresAt <= policy.nowEpochMilliseconds || input.emittedAt > policy.nowEpochMilliseconds + CACHE_INVALIDATION_WIRE_LIMITS.maxFutureClockSkewMs ) { return failure("EXPIRED"); } return { ok: true, value: Object.freeze({ protocolVersion: CACHE_INVALIDATION_PROTOCOL_VERSION, eventId: input.eventId, sourceId: input.sourceId, sourceEpoch: input.sourceEpoch, sequence: input.sequence, cacheEpoch: input.cacheEpoch, topic: input.topic, topicVersion: input.topicVersion, emittedAt: input.emittedAt, expiresAt: input.expiresAt, }), }; } function isExactWireRecord( value: unknown, ): value is Readonly> { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } const keys = Object.keys(value).sort(); return ( keys.length === WIRE_KEYS.length && keys.every((key, index) => key === WIRE_KEYS[index]) ); } function isPositiveSafeInteger(value: unknown): value is number { return ( typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ); } function isEpochMilliseconds(value: unknown): value is number { return ( typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ); } function failure( reason: CacheInvalidationParseFailureReason, ): Extract { return Object.freeze({ ok: false, reason }); }