1+ /*
2+ * Copyright 2024-2026 Embabel Pty Ltd.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+ package com.embabel.chat.event
17+
18+ import com.embabel.chat.Message
19+ import com.embabel.chat.Role
20+ import com.embabel.common.core.types.Timestamped
21+ import java.time.Instant
22+
23+ /* *
24+ * Status of a message in its lifecycle.
25+ */
26+ enum class MessageStatus {
27+ /* *
28+ * Message has been added to the conversation.
29+ *
30+ * For in-memory conversations, this is the terminal state.
31+ * For persistent conversations, this may be followed by [PERSISTED] or [PERSISTENCE_FAILED].
32+ */
33+ ADDED ,
34+
35+ /* *
36+ * Message has been successfully persisted to storage.
37+ *
38+ * Only fired by persistent conversation implementations.
39+ */
40+ PERSISTED ,
41+
42+ /* *
43+ * Message persistence failed.
44+ *
45+ * Only fired by persistent conversation implementations.
46+ * Check [MessageEvent.error] for details.
47+ */
48+ PERSISTENCE_FAILED
49+ }
50+
51+ /* *
52+ * Event published for message lifecycle changes in a conversation.
53+ *
54+ * ## Status Flow
55+ *
56+ * | Conversation Type | Events Fired |
57+ * |-------------------|--------------|
58+ * | In-memory | `ADDED` |
59+ * | Persistent | `ADDED` → `PERSISTED` or `PERSISTENCE_FAILED` |
60+ *
61+ * ## Usage
62+ *
63+ * ```kotlin
64+ * @EventListener
65+ * fun onMessage(event: MessageEvent) {
66+ * when (event.status) {
67+ * MessageStatus.ADDED -> {
68+ * // Message appeared in conversation - show in UI
69+ * }
70+ * MessageStatus.PERSISTED -> {
71+ * // Message saved - can update UI indicator
72+ * }
73+ * MessageStatus.PERSISTENCE_FAILED -> {
74+ * // Handle failure - event.error has details
75+ * }
76+ * }
77+ * }
78+ *
79+ * // Or filter to specific status:
80+ * @EventListener(condition = "#event.status.name() == 'ADDED'")
81+ * fun onMessageAdded(event: MessageEvent) { ... }
82+ * ```
83+ *
84+ * @param conversationId the conversation the message belongs to
85+ * @param status the current status of the message
86+ * @param fromUserId the ID of the user who sent this message (author)
87+ * @param toUserId the ID of the user who should receive this message (for routing, e.g., WebSocket)
88+ * @param message the message (always present for ADDED, present for PERSISTED)
89+ * @param content the message content (useful for PERSISTENCE_FAILED when message ref may be stale)
90+ * @param role the message role
91+ * @param error the exception if persistence failed (present for PERSISTENCE_FAILED)
92+ * @param timestamp when the event occurred
93+ */
94+ data class MessageEvent (
95+ val conversationId : String ,
96+ val status : MessageStatus ,
97+ val fromUserId : String? = null ,
98+ val toUserId : String? = null ,
99+ val message : Message ? = null ,
100+ val content : String? = null ,
101+ val role : Role ? = null ,
102+ val error : Throwable ? = null ,
103+ override val timestamp : Instant = Instant .now()
104+ ) : Timestamped {
105+
106+ companion object {
107+ /* *
108+ * Create an ADDED event - message was added to conversation.
109+ */
110+ fun added (
111+ conversationId : String ,
112+ message : Message ,
113+ fromUserId : String? = null,
114+ toUserId : String? = null
115+ ) = MessageEvent (
116+ conversationId = conversationId,
117+ status = MessageStatus .ADDED ,
118+ fromUserId = fromUserId,
119+ toUserId = toUserId,
120+ message = message,
121+ content = message.content,
122+ role = message.role
123+ )
124+
125+ /* *
126+ * Create a PERSISTED event - message was saved to storage.
127+ */
128+ fun persisted (
129+ conversationId : String ,
130+ message : Message ,
131+ fromUserId : String? = null,
132+ toUserId : String? = null
133+ ) = MessageEvent (
134+ conversationId = conversationId,
135+ status = MessageStatus .PERSISTED ,
136+ fromUserId = fromUserId,
137+ toUserId = toUserId,
138+ message = message,
139+ content = message.content,
140+ role = message.role
141+ )
142+
143+ /* *
144+ * Create a PERSISTENCE_FAILED event.
145+ */
146+ fun persistenceFailed (
147+ conversationId : String ,
148+ content : String ,
149+ role : Role ,
150+ error : Throwable ,
151+ fromUserId : String? = null,
152+ toUserId : String? = null
153+ ) = MessageEvent (
154+ conversationId = conversationId,
155+ status = MessageStatus .PERSISTENCE_FAILED ,
156+ fromUserId = fromUserId,
157+ toUserId = toUserId,
158+ content = content,
159+ role = role,
160+ error = error
161+ )
162+ }
163+ }
0 commit comments