001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.time.Duration; 024 025import static java.util.Objects.requireNonNull; 026 027/** 028 * Immutable MCP application-cache policy. 029 * 030 * <p>The policy fixes both the cache scope and the default time to live for 031 * its owning operation. A result may override only the time to live where the 032 * operation's result type explicitly permits it. 033 * 034 * <p>The cache vocabulary is revision-sensitive rather than a permanent 035 * closed enum contract. Code switching on cache scope should retain a 036 * forward-compatible default for a future protocol revision. 037 * 038 * @author <a href="https://www.revetkn.com">Mark Allen</a> 039 */ 040@ThreadSafe 041public final class McpCachePolicy { 042 @NonNull 043 private static final McpCachePolicy PRIVATE_NO_CACHE = 044 new McpCachePolicy(Duration.ZERO, McpCacheScope.PRIVATE); 045 @NonNull 046 private final Duration timeToLive; 047 @NonNull 048 private final McpCacheScope scope; 049 050 private McpCachePolicy(@NonNull Duration timeToLive, 051 @NonNull McpCacheScope scope) { 052 this.timeToLive = requireTimeToLive(timeToLive); 053 this.scope = requireNonNull(scope); 054 } 055 056 /** 057 * Returns the shared private policy with a zero time to live. 058 * 059 * @return private zero-TTL policy 060 */ 061 @NonNull 062 public static McpCachePolicy privateNoCacheInstance() { 063 return PRIVATE_NO_CACHE; 064 } 065 066 /** 067 * Creates a private policy with the supplied default time to live. 068 * 069 * @param timeToLive nonnegative whole-millisecond default duration 070 * @return private cache policy 071 */ 072 @NonNull 073 public static McpCachePolicy fromPrivateTimeToLive( 074 @NonNull Duration timeToLive) { 075 return new McpCachePolicy(timeToLive, McpCacheScope.PRIVATE); 076 } 077 078 /** 079 * Creates a public policy with the supplied default time to live. 080 * 081 * <p>Callers must use public scope only when results do not vary by 082 * principal, tenant, authorization partition, or other private identity. 083 * 084 * @param timeToLive nonnegative whole-millisecond default duration 085 * @return public cache policy 086 */ 087 @NonNull 088 public static McpCachePolicy fromPublicTimeToLive( 089 @NonNull Duration timeToLive) { 090 return new McpCachePolicy(timeToLive, McpCacheScope.PUBLIC); 091 } 092 093 /** @return nonnegative whole-millisecond default time to live */ 094 @NonNull 095 public Duration getTimeToLive() { 096 return this.timeToLive; 097 } 098 099 /** @return fixed cache scope */ 100 @NonNull 101 public McpCacheScope getScope() { 102 return this.scope; 103 } 104 105 /** @return whether the cache scope and time to live are equal */ 106 @Override 107 public boolean equals(@Nullable Object other) { 108 return this == other 109 || other instanceof McpCachePolicy policy 110 && this.timeToLive.equals(policy.timeToLive) 111 && this.scope == policy.scope; 112 } 113 114 /** @return structural cache-policy hash code */ 115 @Override 116 public int hashCode() { 117 int result = this.timeToLive.hashCode(); 118 result = 31 * result + this.scope.hashCode(); 119 return result; 120 } 121 122 @NonNull 123 static Duration requireTimeToLive(@NonNull Duration timeToLive) { 124 requireNonNull(timeToLive); 125 if (timeToLive.isNegative() 126 || timeToLive.getNano() % 1_000_000 != 0) 127 throw new IllegalArgumentException( 128 "Cache time to live must be a nonnegative whole-millisecond duration."); 129 try { 130 return Duration.ofMillis(timeToLive.toMillis()); 131 } catch (ArithmeticException exception) { 132 throw new IllegalArgumentException( 133 "Cache time to live must fit in a signed 64-bit millisecond value.", 134 exception); 135 } 136 } 137}