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}