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}