WorkflowSessionEvent.java
package org.hammer.audio.workflow.collaboration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.workflow.Workflow;
/**
* Ordered transport-neutral event emitted by one collaboration session.
*
* <p>Semantic workflow state is carried as an immutable {@link Workflow} snapshot. HTTP adapters
* may derive renderer-specific projections from it, while the collaboration package stays
* independent of editor and visualization types.
*
* @param eventId stable event identifier
* @param sessionId collaboration session identifier
* @param sequence monotonically increasing sequence within the session
* @param revision monotonically increasing semantic workflow revision
* @param occurredAt server timestamp for the event
* @param type event type
* @param actor actor associated with the event, or {@code null} for a replay snapshot
* @param operationId accepted semantic operation identifier, or {@code null}
* @param workflow canonical workflow snapshot for semantic/snapshot events, or {@code null}
* @param attributes immutable presence or operation metadata
*/
public record WorkflowSessionEvent(
String eventId,
String sessionId,
long sequence,
long revision,
Instant occurredAt,
Type type,
OperationActor actor,
String operationId,
Workflow workflow,
Map<String, String> attributes) {
/** Event types exposed to collaboration transports. */
public enum Type {
SESSION_CREATED,
SESSION_CLOSED,
OPERATION_ACCEPTED,
PRESENCE_JOINED,
PRESENCE_UPDATED,
PRESENCE_LEFT,
SNAPSHOT
}
public WorkflowSessionEvent {
eventId = requireNotBlank(eventId, "eventId");
sessionId = requireNotBlank(sessionId, "sessionId");
if (sequence <= 0) {
throw new IllegalArgumentException("sequence must be > 0");
}
if (revision < 0) {
throw new IllegalArgumentException("revision must be >= 0");
}
occurredAt = Objects.requireNonNull(occurredAt, "occurredAt");
type = Objects.requireNonNull(type, "type");
attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
validatePayload(type, actor, operationId, workflow);
}
private static void validatePayload(
Type type, OperationActor actor, String operationId, Workflow workflow) {
switch (type) {
case SESSION_CREATED -> {
Objects.requireNonNull(actor, "actor");
Objects.requireNonNull(workflow, "workflow");
}
case OPERATION_ACCEPTED -> {
Objects.requireNonNull(actor, "actor");
requireNotBlank(operationId, "operationId");
Objects.requireNonNull(workflow, "workflow");
}
case PRESENCE_JOINED, PRESENCE_UPDATED, PRESENCE_LEFT, SESSION_CLOSED ->
Objects.requireNonNull(actor, "actor");
case SNAPSHOT -> Objects.requireNonNull(workflow, "workflow");
}
}
private static String requireNotBlank(String value, String field) {
Objects.requireNonNull(value, field);
if (value.isBlank()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value;
}
}