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;
020
021import javax.annotation.concurrent.ThreadSafe;
022
023/**
024 * Thread-safe broadcast publisher for application-owned MCP task-change
025 * events.
026 * <p>
027 * Events use broadcast rather than competing-consumer semantics: one listener
028 * receiving an event must not consume it on behalf of the others.
029 * Implementations may perform process-local delivery or coordinate through a
030 * distributed service, and may provide best-effort rather than durable
031 * delivery.
032 * In a fleet, the application normally supplies a publisher that reaches every
033 * Soklet node capable of serving the corresponding task. Soklet closes only
034 * registrations returned by {@link #subscribe(McpTaskEventListener)} and never
035 * closes the publisher itself.
036 * <p>
037 * An event is a non-authoritative hint that carries only a task ID. It must be
038 * published after the corresponding state change is durable and readable
039 * through the associated {@link McpTaskManager}. Duplicate, delayed, or lost
040 * events are permitted because polling remains authoritative. Soklet performs
041 * a fresh manager lookup using each independently admitted subscription's
042 * endpoint and identity before rendering a task snapshot. A publisher must
043 * protect task IDs as potentially sensitive application data and must not
044 * attach task state, principals, or authorization decisions to an event.
045 *
046 * @author <a href="https://www.revetkn.com">Mark Allen</a>
047 */
048@ThreadSafe
049public interface McpTaskEventPublisher {
050        /**
051         * Creates an independent in-memory publisher with default behavior.
052         * <p>
053         * The publisher reaches only listeners registered with that instance in the
054         * current process and invokes them synchronously on the publishing thread.
055         * Closing a registration does not wait for a delivery already selected or
056         * in flight. If a listener throws a runtime exception, all other current
057         * listeners are still attempted before the first exception is rethrown with
058         * later exceptions suppressed.
059         *
060         * @return in-memory task-event publisher
061         */
062        @NonNull
063        static McpTaskEventPublisher fromInMemoryDefaults() {
064                return new DefaultMcpTaskEventPublisher();
065        }
066
067        /**
068         * Registers a listener for broadcast task-change events.
069         *
070         * @param listener thread-safe listener
071         * @return an idempotently closable listener registration
072         */
073        @NonNull
074        McpSubscriptionEventRegistration subscribe(
075                        @NonNull McpTaskEventListener listener);
076
077        /**
078         * Broadcasts a coarse change event after the task's new durable state is
079         * readable through the associated task manager.
080         *
081         * @param taskId changed task identifier
082         * @throws NullPointerException if {@code taskId} is null
083         * @throws IllegalArgumentException if {@code taskId} is blank or contains a
084         * carriage-return or newline character
085         */
086        void publishTaskChanged(@NonNull String taskId);
087}