solid_mcp 0.2.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/solid_mcp_native/Cargo.toml +12 -0
- data/ext/solid_mcp_native/core/Cargo.toml +32 -0
- data/ext/solid_mcp_native/core/src/config.rs +133 -0
- data/ext/solid_mcp_native/core/src/db/mod.rs +154 -0
- data/ext/solid_mcp_native/core/src/db/postgres.rs +242 -0
- data/ext/solid_mcp_native/core/src/db/sqlite.rs +276 -0
- data/ext/solid_mcp_native/core/src/error.rs +38 -0
- data/ext/solid_mcp_native/core/src/lib.rs +25 -0
- data/ext/solid_mcp_native/core/src/message.rs +191 -0
- data/ext/solid_mcp_native/core/src/pubsub.rs +309 -0
- data/ext/solid_mcp_native/core/src/subscriber.rs +298 -0
- data/ext/solid_mcp_native/core/src/writer.rs +252 -0
- data/ext/solid_mcp_native/extconf.rb +3 -0
- data/ext/solid_mcp_native/ffi/Cargo.toml +20 -0
- data/ext/solid_mcp_native/ffi/extconf.rb +67 -0
- data/ext/solid_mcp_native/ffi/src/lib.rs +224 -0
- data/lib/solid_mcp/configuration.rb +5 -2
- data/lib/solid_mcp/message_writer.rb +80 -45
- data/lib/solid_mcp/native_speedup.rb +140 -0
- data/lib/solid_mcp/pub_sub.rb +10 -8
- data/lib/solid_mcp/subscriber.rb +18 -7
- data/lib/solid_mcp/version.rb +1 -1
- data/lib/solid_mcp.rb +3 -0
- metadata +57 -19
- data/.release-please-manifest.json +0 -1
- data/CHANGELOG.md +0 -34
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -140
- data/Rakefile +0 -8
- data/app/models/solid_mcp/message.rb +0 -25
- data/app/models/solid_mcp/record.rb +0 -10
- data/bin/console +0 -11
- data/bin/rails +0 -15
- data/bin/setup +0 -8
- data/bin/test +0 -8
- data/db/migrate/20250624000001_create_solid_mcp_messages.rb +0 -28
- data/release-please-config.json +0 -8
- data/solid_mcp.gemspec +0 -39
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//! SQLite database backend for solid-mcp-core
|
|
2
|
+
|
|
3
|
+
use crate::{Message, Result};
|
|
4
|
+
use async_trait::async_trait;
|
|
5
|
+
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
|
6
|
+
use sqlx::{Pool, Sqlite};
|
|
7
|
+
use std::str::FromStr;
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
|
|
10
|
+
/// SQLite connection pool
|
|
11
|
+
#[derive(Clone)]
|
|
12
|
+
pub struct SqlitePool {
|
|
13
|
+
pool: Pool<Sqlite>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl SqlitePool {
|
|
17
|
+
/// Create a new SQLite pool from a database URL
|
|
18
|
+
///
|
|
19
|
+
/// The database and tables must already exist (created by Ruby migrations).
|
|
20
|
+
pub async fn new(database_url: &str) -> Result<Self> {
|
|
21
|
+
// Parse the URL and configure for WAL mode (better concurrency)
|
|
22
|
+
let options = SqliteConnectOptions::from_str(database_url)?
|
|
23
|
+
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
|
24
|
+
.synchronous(sqlx::sqlite::SqliteSynchronous::Normal)
|
|
25
|
+
.busy_timeout(Duration::from_secs(30));
|
|
26
|
+
|
|
27
|
+
let pool = SqlitePoolOptions::new()
|
|
28
|
+
.max_connections(1) // SQLite works best with single writer
|
|
29
|
+
.connect_with(options)
|
|
30
|
+
.await?;
|
|
31
|
+
|
|
32
|
+
Ok(Self { pool })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Create tables for testing purposes only
|
|
36
|
+
#[cfg(test)]
|
|
37
|
+
pub(crate) async fn setup_test_schema(&self) -> Result<()> {
|
|
38
|
+
sqlx::query(
|
|
39
|
+
r#"
|
|
40
|
+
CREATE TABLE IF NOT EXISTS solid_mcp_messages (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
session_id TEXT NOT NULL,
|
|
43
|
+
event_type TEXT NOT NULL,
|
|
44
|
+
data TEXT NOT NULL,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
delivered_at TEXT
|
|
47
|
+
)
|
|
48
|
+
"#,
|
|
49
|
+
)
|
|
50
|
+
.execute(&self.pool)
|
|
51
|
+
.await?;
|
|
52
|
+
|
|
53
|
+
sqlx::query(
|
|
54
|
+
r#"
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_solid_mcp_messages_session_id
|
|
56
|
+
ON solid_mcp_messages(session_id, id)
|
|
57
|
+
"#,
|
|
58
|
+
)
|
|
59
|
+
.execute(&self.pool)
|
|
60
|
+
.await?;
|
|
61
|
+
|
|
62
|
+
sqlx::query(
|
|
63
|
+
r#"
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_solid_mcp_messages_delivered
|
|
65
|
+
ON solid_mcp_messages(delivered_at, created_at)
|
|
66
|
+
"#,
|
|
67
|
+
)
|
|
68
|
+
.execute(&self.pool)
|
|
69
|
+
.await?;
|
|
70
|
+
|
|
71
|
+
Ok(())
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[async_trait]
|
|
76
|
+
impl super::Database for SqlitePool {
|
|
77
|
+
async fn insert_batch(&self, messages: &[Message]) -> Result<()> {
|
|
78
|
+
if messages.is_empty() {
|
|
79
|
+
return Ok(());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build batch insert query
|
|
83
|
+
let mut query = String::from(
|
|
84
|
+
"INSERT INTO solid_mcp_messages (session_id, event_type, data, created_at) VALUES ",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let mut params: Vec<String> = Vec::with_capacity(messages.len() * 4);
|
|
88
|
+
|
|
89
|
+
for (i, msg) in messages.iter().enumerate() {
|
|
90
|
+
if i > 0 {
|
|
91
|
+
query.push_str(", ");
|
|
92
|
+
}
|
|
93
|
+
let base = i * 4 + 1;
|
|
94
|
+
query.push_str(&format!(
|
|
95
|
+
"(${}, ${}, ${}, ${})",
|
|
96
|
+
base,
|
|
97
|
+
base + 1,
|
|
98
|
+
base + 2,
|
|
99
|
+
base + 3
|
|
100
|
+
));
|
|
101
|
+
params.push(msg.session_id.clone());
|
|
102
|
+
params.push(msg.event_type.clone());
|
|
103
|
+
params.push(msg.data.clone());
|
|
104
|
+
params.push(msg.created_at.to_rfc3339());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Execute with parameters
|
|
108
|
+
let mut q = sqlx::query(&query);
|
|
109
|
+
for param in ¶ms {
|
|
110
|
+
q = q.bind(param);
|
|
111
|
+
}
|
|
112
|
+
q.execute(&self.pool).await?;
|
|
113
|
+
|
|
114
|
+
Ok(())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async fn fetch_after(
|
|
118
|
+
&self,
|
|
119
|
+
session_id: &str,
|
|
120
|
+
after_id: i64,
|
|
121
|
+
limit: i64,
|
|
122
|
+
) -> Result<Vec<Message>> {
|
|
123
|
+
let rows = sqlx::query_as::<_, (i64, String, String, String, String, Option<String>)>(
|
|
124
|
+
r#"
|
|
125
|
+
SELECT id, session_id, event_type, data, created_at, delivered_at
|
|
126
|
+
FROM solid_mcp_messages
|
|
127
|
+
WHERE session_id = $1 AND delivered_at IS NULL AND id > $2
|
|
128
|
+
ORDER BY id
|
|
129
|
+
LIMIT $3
|
|
130
|
+
"#,
|
|
131
|
+
)
|
|
132
|
+
.bind(session_id)
|
|
133
|
+
.bind(after_id)
|
|
134
|
+
.bind(limit)
|
|
135
|
+
.fetch_all(&self.pool)
|
|
136
|
+
.await?;
|
|
137
|
+
|
|
138
|
+
let messages = rows
|
|
139
|
+
.into_iter()
|
|
140
|
+
.map(
|
|
141
|
+
|(id, session_id, event_type, data, created_at, delivered_at)| Message {
|
|
142
|
+
id,
|
|
143
|
+
session_id,
|
|
144
|
+
event_type,
|
|
145
|
+
data,
|
|
146
|
+
created_at: chrono::DateTime::parse_from_rfc3339(&created_at)
|
|
147
|
+
.unwrap_or_default()
|
|
148
|
+
.with_timezone(&chrono::Utc),
|
|
149
|
+
delivered_at: delivered_at.and_then(|d| {
|
|
150
|
+
chrono::DateTime::parse_from_rfc3339(&d)
|
|
151
|
+
.ok()
|
|
152
|
+
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
.collect();
|
|
157
|
+
|
|
158
|
+
Ok(messages)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async fn mark_delivered(&self, ids: &[i64]) -> Result<()> {
|
|
162
|
+
if ids.is_empty() {
|
|
163
|
+
return Ok(());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// $1 is for the timestamp, ids start from $2
|
|
167
|
+
let placeholders: Vec<String> = (2..=ids.len() + 1).map(|i| format!("${}", i)).collect();
|
|
168
|
+
let query = format!(
|
|
169
|
+
"UPDATE solid_mcp_messages SET delivered_at = $1 WHERE id IN ({})",
|
|
170
|
+
placeholders.join(", ")
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let now = chrono::Utc::now().to_rfc3339();
|
|
174
|
+
let mut q = sqlx::query(&query).bind(&now);
|
|
175
|
+
for id in ids {
|
|
176
|
+
q = q.bind(id);
|
|
177
|
+
}
|
|
178
|
+
q.execute(&self.pool).await?;
|
|
179
|
+
|
|
180
|
+
Ok(())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async fn cleanup_delivered(&self, older_than: Duration) -> Result<u64> {
|
|
184
|
+
let cutoff =
|
|
185
|
+
(chrono::Utc::now() - chrono::Duration::from_std(older_than).unwrap()).to_rfc3339();
|
|
186
|
+
|
|
187
|
+
let result = sqlx::query(
|
|
188
|
+
r#"
|
|
189
|
+
DELETE FROM solid_mcp_messages
|
|
190
|
+
WHERE delivered_at IS NOT NULL AND delivered_at < $1
|
|
191
|
+
"#,
|
|
192
|
+
)
|
|
193
|
+
.bind(&cutoff)
|
|
194
|
+
.execute(&self.pool)
|
|
195
|
+
.await?;
|
|
196
|
+
|
|
197
|
+
Ok(result.rows_affected())
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async fn cleanup_undelivered(&self, older_than: Duration) -> Result<u64> {
|
|
201
|
+
let cutoff =
|
|
202
|
+
(chrono::Utc::now() - chrono::Duration::from_std(older_than).unwrap()).to_rfc3339();
|
|
203
|
+
|
|
204
|
+
let result = sqlx::query(
|
|
205
|
+
r#"
|
|
206
|
+
DELETE FROM solid_mcp_messages
|
|
207
|
+
WHERE delivered_at IS NULL AND created_at < $1
|
|
208
|
+
"#,
|
|
209
|
+
)
|
|
210
|
+
.bind(&cutoff)
|
|
211
|
+
.execute(&self.pool)
|
|
212
|
+
.await?;
|
|
213
|
+
|
|
214
|
+
Ok(result.rows_affected())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async fn max_id(&self) -> Result<i64> {
|
|
218
|
+
let row: (Option<i64>,) = sqlx::query_as("SELECT MAX(id) FROM solid_mcp_messages")
|
|
219
|
+
.fetch_one(&self.pool)
|
|
220
|
+
.await?;
|
|
221
|
+
|
|
222
|
+
Ok(row.0.unwrap_or(0))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[cfg(test)]
|
|
227
|
+
mod tests {
|
|
228
|
+
use super::*;
|
|
229
|
+
use crate::db::Database;
|
|
230
|
+
|
|
231
|
+
async fn create_test_pool() -> SqlitePool {
|
|
232
|
+
let pool = SqlitePool::new("sqlite::memory:").await.unwrap();
|
|
233
|
+
pool.setup_test_schema().await.unwrap();
|
|
234
|
+
pool
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[tokio::test]
|
|
238
|
+
async fn test_sqlite_pool_creation() {
|
|
239
|
+
let pool = create_test_pool().await;
|
|
240
|
+
assert_eq!(pool.max_id().await.unwrap(), 0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[tokio::test]
|
|
244
|
+
async fn test_insert_and_fetch() {
|
|
245
|
+
let pool = create_test_pool().await;
|
|
246
|
+
|
|
247
|
+
let messages = vec![
|
|
248
|
+
Message::new("session-1", "message", r#"{"test":1}"#),
|
|
249
|
+
Message::new("session-1", "message", r#"{"test":2}"#),
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
pool.insert_batch(&messages).await.unwrap();
|
|
253
|
+
|
|
254
|
+
let fetched = pool.fetch_after("session-1", 0, 100).await.unwrap();
|
|
255
|
+
assert_eq!(fetched.len(), 2);
|
|
256
|
+
assert_eq!(fetched[0].data, r#"{"test":1}"#);
|
|
257
|
+
assert_eq!(fetched[1].data, r#"{"test":2}"#);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[tokio::test]
|
|
261
|
+
async fn test_mark_delivered() {
|
|
262
|
+
let pool = create_test_pool().await;
|
|
263
|
+
|
|
264
|
+
let messages = vec![Message::new("session-1", "message", r#"{}"#)];
|
|
265
|
+
pool.insert_batch(&messages).await.unwrap();
|
|
266
|
+
|
|
267
|
+
let fetched = pool.fetch_after("session-1", 0, 100).await.unwrap();
|
|
268
|
+
assert_eq!(fetched.len(), 1);
|
|
269
|
+
|
|
270
|
+
pool.mark_delivered(&[fetched[0].id]).await.unwrap();
|
|
271
|
+
|
|
272
|
+
// Should not fetch delivered messages
|
|
273
|
+
let fetched = pool.fetch_after("session-1", 0, 100).await.unwrap();
|
|
274
|
+
assert_eq!(fetched.len(), 0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//! Error types for solid-mcp-core
|
|
2
|
+
|
|
3
|
+
use thiserror::Error;
|
|
4
|
+
|
|
5
|
+
/// Result type alias using our Error type
|
|
6
|
+
pub type Result<T> = std::result::Result<T, Error>;
|
|
7
|
+
|
|
8
|
+
/// Errors that can occur in solid-mcp-core
|
|
9
|
+
#[derive(Error, Debug)]
|
|
10
|
+
pub enum Error {
|
|
11
|
+
/// Database operation failed
|
|
12
|
+
#[error("database error: {0}")]
|
|
13
|
+
Database(#[from] sqlx::Error),
|
|
14
|
+
|
|
15
|
+
/// JSON serialization/deserialization failed
|
|
16
|
+
#[error("json error: {0}")]
|
|
17
|
+
Json(#[from] serde_json::Error),
|
|
18
|
+
|
|
19
|
+
/// Channel send failed (queue full or shutdown)
|
|
20
|
+
#[error("channel send error: queue full or shutdown")]
|
|
21
|
+
ChannelSend,
|
|
22
|
+
|
|
23
|
+
/// Channel receive failed (shutdown)
|
|
24
|
+
#[error("channel receive error: shutdown")]
|
|
25
|
+
ChannelRecv,
|
|
26
|
+
|
|
27
|
+
/// Configuration error
|
|
28
|
+
#[error("configuration error: {0}")]
|
|
29
|
+
Config(String),
|
|
30
|
+
|
|
31
|
+
/// Shutdown requested
|
|
32
|
+
#[error("shutdown requested")]
|
|
33
|
+
Shutdown,
|
|
34
|
+
|
|
35
|
+
/// Session not found
|
|
36
|
+
#[error("session not found: {0}")]
|
|
37
|
+
SessionNotFound(String),
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//! # solid-mcp-core
|
|
2
|
+
//!
|
|
3
|
+
//! High-performance async pub/sub engine for MCP (Model Context Protocol).
|
|
4
|
+
//!
|
|
5
|
+
//! This crate provides the core functionality for solid_mcp:
|
|
6
|
+
//! - Async message writing with batching
|
|
7
|
+
//! - Session-based subscriptions with PostgreSQL LISTEN/NOTIFY or SQLite polling
|
|
8
|
+
//! - Database-backed message persistence
|
|
9
|
+
//!
|
|
10
|
+
//! ## Features
|
|
11
|
+
//! - `sqlite` - Enable SQLite backend (default)
|
|
12
|
+
//! - `postgres` - Enable PostgreSQL backend with LISTEN/NOTIFY (default)
|
|
13
|
+
|
|
14
|
+
pub mod config;
|
|
15
|
+
pub mod db;
|
|
16
|
+
pub mod error;
|
|
17
|
+
pub mod message;
|
|
18
|
+
pub mod pubsub;
|
|
19
|
+
pub mod subscriber;
|
|
20
|
+
pub mod writer;
|
|
21
|
+
|
|
22
|
+
pub use config::Config;
|
|
23
|
+
pub use error::{Error, Result};
|
|
24
|
+
pub use message::Message;
|
|
25
|
+
pub use pubsub::PubSub;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//! Message type for solid-mcp-core
|
|
2
|
+
|
|
3
|
+
use chrono::{DateTime, Utc};
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
|
|
6
|
+
/// A message in the pub/sub system
|
|
7
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
|
+
pub struct Message {
|
|
9
|
+
/// Unique message ID (database primary key)
|
|
10
|
+
#[serde(default)]
|
|
11
|
+
pub id: i64,
|
|
12
|
+
|
|
13
|
+
/// Session ID this message belongs to (UUID format, 36 chars)
|
|
14
|
+
pub session_id: String,
|
|
15
|
+
|
|
16
|
+
/// Event type (e.g., "message", "ping", "notification")
|
|
17
|
+
pub event_type: String,
|
|
18
|
+
|
|
19
|
+
/// JSON payload
|
|
20
|
+
pub data: String,
|
|
21
|
+
|
|
22
|
+
/// When the message was created
|
|
23
|
+
pub created_at: DateTime<Utc>,
|
|
24
|
+
|
|
25
|
+
/// When the message was delivered (None = undelivered)
|
|
26
|
+
#[serde(default)]
|
|
27
|
+
pub delivered_at: Option<DateTime<Utc>>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Message {
|
|
31
|
+
/// Create a new message (id will be set by database)
|
|
32
|
+
pub fn new(
|
|
33
|
+
session_id: impl Into<String>,
|
|
34
|
+
event_type: impl Into<String>,
|
|
35
|
+
data: impl Into<String>,
|
|
36
|
+
) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
id: 0,
|
|
39
|
+
session_id: session_id.into(),
|
|
40
|
+
event_type: event_type.into(),
|
|
41
|
+
data: data.into(),
|
|
42
|
+
created_at: Utc::now(),
|
|
43
|
+
delivered_at: None,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Create a message with JSON data
|
|
48
|
+
pub fn with_json<T: Serialize>(
|
|
49
|
+
session_id: impl Into<String>,
|
|
50
|
+
event_type: impl Into<String>,
|
|
51
|
+
data: &T,
|
|
52
|
+
) -> Result<Self, serde_json::Error> {
|
|
53
|
+
let json = serde_json::to_string(data)?;
|
|
54
|
+
Ok(Self::new(session_id, event_type, json))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Check if this message has been delivered
|
|
58
|
+
pub fn is_delivered(&self) -> bool {
|
|
59
|
+
self.delivered_at.is_some()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Mark this message as delivered now
|
|
63
|
+
pub fn mark_delivered(&mut self) {
|
|
64
|
+
self.delivered_at = Some(Utc::now());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Batch of messages for efficient database operations
|
|
69
|
+
#[derive(Debug, Default)]
|
|
70
|
+
pub struct MessageBatch {
|
|
71
|
+
messages: Vec<Message>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl MessageBatch {
|
|
75
|
+
/// Create a new empty batch
|
|
76
|
+
pub fn new() -> Self {
|
|
77
|
+
Self::default()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Create a batch with pre-allocated capacity
|
|
81
|
+
pub fn with_capacity(capacity: usize) -> Self {
|
|
82
|
+
Self {
|
|
83
|
+
messages: Vec::with_capacity(capacity),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Add a message to the batch
|
|
88
|
+
pub fn push(&mut self, message: Message) {
|
|
89
|
+
self.messages.push(message);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Get the number of messages in the batch
|
|
93
|
+
pub fn len(&self) -> usize {
|
|
94
|
+
self.messages.len()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Check if the batch is empty
|
|
98
|
+
pub fn is_empty(&self) -> bool {
|
|
99
|
+
self.messages.is_empty()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Clear the batch
|
|
103
|
+
pub fn clear(&mut self) {
|
|
104
|
+
self.messages.clear();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Get the messages as a slice
|
|
108
|
+
pub fn as_slice(&self) -> &[Message] {
|
|
109
|
+
&self.messages
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Take ownership of the messages
|
|
113
|
+
pub fn into_vec(self) -> Vec<Message> {
|
|
114
|
+
self.messages
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Iterate over messages
|
|
118
|
+
pub fn iter(&self) -> impl Iterator<Item = &Message> {
|
|
119
|
+
self.messages.iter()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
impl IntoIterator for MessageBatch {
|
|
124
|
+
type Item = Message;
|
|
125
|
+
type IntoIter = std::vec::IntoIter<Message>;
|
|
126
|
+
|
|
127
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
128
|
+
self.messages.into_iter()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl FromIterator<Message> for MessageBatch {
|
|
133
|
+
fn from_iter<T: IntoIterator<Item = Message>>(iter: T) -> Self {
|
|
134
|
+
Self {
|
|
135
|
+
messages: iter.into_iter().collect(),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[cfg(test)]
|
|
141
|
+
mod tests {
|
|
142
|
+
use super::*;
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_message_new() {
|
|
146
|
+
let msg = Message::new("session-123", "message", r#"{"hello":"world"}"#);
|
|
147
|
+
assert_eq!(msg.session_id, "session-123");
|
|
148
|
+
assert_eq!(msg.event_type, "message");
|
|
149
|
+
assert_eq!(msg.data, r#"{"hello":"world"}"#);
|
|
150
|
+
assert!(!msg.is_delivered());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn test_message_with_json() {
|
|
155
|
+
#[derive(Serialize)]
|
|
156
|
+
struct Payload {
|
|
157
|
+
hello: String,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let payload = Payload {
|
|
161
|
+
hello: "world".to_string(),
|
|
162
|
+
};
|
|
163
|
+
let msg = Message::with_json("session-123", "message", &payload).unwrap();
|
|
164
|
+
assert_eq!(msg.data, r#"{"hello":"world"}"#);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn test_mark_delivered() {
|
|
169
|
+
let mut msg = Message::new("session-123", "message", "{}");
|
|
170
|
+
assert!(!msg.is_delivered());
|
|
171
|
+
|
|
172
|
+
msg.mark_delivered();
|
|
173
|
+
assert!(msg.is_delivered());
|
|
174
|
+
assert!(msg.delivered_at.is_some());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn test_message_batch() {
|
|
179
|
+
let mut batch = MessageBatch::with_capacity(10);
|
|
180
|
+
assert!(batch.is_empty());
|
|
181
|
+
|
|
182
|
+
batch.push(Message::new("s1", "msg", "{}"));
|
|
183
|
+
batch.push(Message::new("s2", "msg", "{}"));
|
|
184
|
+
|
|
185
|
+
assert_eq!(batch.len(), 2);
|
|
186
|
+
assert!(!batch.is_empty());
|
|
187
|
+
|
|
188
|
+
let messages: Vec<_> = batch.into_iter().collect();
|
|
189
|
+
assert_eq!(messages.len(), 2);
|
|
190
|
+
}
|
|
191
|
+
}
|