1#[cfg(feature = "local")]
4mod local;
5#[cfg(feature = "client")]
6mod remote;
7
8#[cfg(all(feature = "client", feature = "local"))]
9use crate::{MemoryAccess, MemorySource, secrets::contains_likely_secret};
10use crate::{
11 MemoryKey, MemoryLimits, MemoryRecord, MemoryScan,
12 server::protocol::{self, ExportCursor, SyncReport},
13};
14#[cfg(feature = "local")]
15pub use local::LocalMemoryStore;
16#[cfg(feature = "client")]
17pub use remote::{RemoteClientError, RemoteMemoryClient, RemoteToken};
18#[cfg(all(feature = "client", feature = "local"))]
19use std::path::PathBuf;
20#[cfg(feature = "local")]
21use std::time::{SystemTime, UNIX_EPOCH};
22use std::{error::Error, future::Future};
23use thiserror::Error;
24
25pub trait MemoryStore: Clone + Send + Sync + 'static {
33 fn scan(
35 &self,
36 query: &str,
37 limit: usize,
38 ) -> impl Future<Output = Result<MemoryScan, MemoryError>> + Send;
39
40 fn read(
42 &self,
43 ids: &[i64],
44 keys: &[MemoryKey],
45 ) -> impl Future<Output = Result<Vec<MemoryRecord>, MemoryError>> + Send;
46
47 fn list(&self) -> impl Future<Output = Result<Vec<MemoryRecord>, MemoryError>> + Send;
52
53 fn put(
55 &self,
56 content: &str,
57 replacement: Option<MemoryKey>,
58 ) -> impl Future<Output = Result<MemoryRecord, MemoryError>> + Send;
59
60 fn delete(&self, key: MemoryKey) -> impl Future<Output = Result<(), MemoryError>> + Send;
65
66 fn sync(
71 &self,
72 memories: &[MemoryRecord],
73 ) -> impl Future<Output = Result<SyncReport, MemoryError>> + Send;
74
75 fn export_page(
81 &self,
82 namespaces: Option<&[String]>,
83 cursor: Option<&ExportCursor>,
84 limit: usize,
85 ) -> impl Future<Output = Result<(Vec<MemoryRecord>, Option<ExportCursor>), MemoryError>> + Send;
86
87 fn export_all(
92 &self,
93 namespaces: Option<&[String]>,
94 ) -> impl Future<Output = Result<Vec<MemoryRecord>, MemoryError>> + Send {
95 async move {
96 let mut cursor = None;
97 let mut records = Vec::new();
98 let mut content_bytes = 0usize;
99 loop {
100 let (page, next_cursor) = self
101 .export_page(
102 namespaces,
103 cursor.as_ref(),
104 protocol::MAX_EXPORT_PAGE_RECORDS,
105 )
106 .await?;
107 let next_record_count = records
108 .len()
109 .checked_add(page.len())
110 .ok_or(MemoryError::InvalidPagination)?;
111 let page_bytes = page.iter().try_fold(0usize, |total, record| {
112 total.checked_add(record.content.len())
113 });
114 content_bytes = content_bytes
115 .checked_add(page_bytes.ok_or(MemoryError::InvalidPagination)?)
116 .ok_or(MemoryError::InvalidPagination)?;
117 if next_record_count > MemoryLimits::PRODUCTION.records
118 || content_bytes > MemoryLimits::PRODUCTION.total_content_bytes
119 || next_cursor
120 .as_ref()
121 .is_some_and(|next| cursor.as_ref() == Some(next))
122 || (page.is_empty() && next_cursor.is_some())
123 {
124 return Err(MemoryError::InvalidPagination);
125 }
126 records.extend(page);
127 match next_cursor {
128 Some(next) => cursor = Some(next),
129 None => return Ok(records),
130 }
131 }
132 }
133 }
134}
135
136#[cfg(all(feature = "client", feature = "local"))]
138#[derive(Clone, Debug)]
139pub enum SelectedMemoryStore {
140 Local(LocalMemoryStore),
142 Remote(RemoteMemoryClient),
144}
145
146#[cfg(all(feature = "client", feature = "local"))]
147impl SelectedMemoryStore {
148 pub fn local(path: impl Into<PathBuf>) -> Self {
150 Self::Local(LocalMemoryStore::new(path))
151 }
152
153 pub const fn remote(client: RemoteMemoryClient) -> Self {
155 Self::Remote(client)
156 }
157
158 pub const fn source(&self) -> MemorySource {
160 match self {
161 Self::Local(_) => MemorySource::Local,
162 Self::Remote(_) => MemorySource::Remote,
163 }
164 }
165
166 pub async fn access(&self) -> Result<MemoryAccess, MemoryError> {
168 match self {
169 Self::Local(_) => Ok(MemoryAccess {
170 source: MemorySource::Local,
171 namespace: None,
172 role: None,
173 }),
174 Self::Remote(client) => Ok(MemoryAccess {
175 source: MemorySource::Remote,
176 namespace: Some(client.namespace().to_owned()),
177 role: Some(client.session().await?),
178 }),
179 }
180 }
181}
182
183#[cfg(all(feature = "client", feature = "local"))]
184impl MemoryStore for SelectedMemoryStore {
185 fn scan(
186 &self,
187 query: &str,
188 limit: usize,
189 ) -> impl Future<Output = Result<MemoryScan, MemoryError>> + Send {
190 async move {
191 match self {
192 Self::Local(store) => MemoryStore::scan(store, query, limit).await,
193 Self::Remote(client) => MemoryStore::scan(client, query, limit).await,
194 }
195 }
196 }
197 fn read(
198 &self,
199 ids: &[i64],
200 keys: &[MemoryKey],
201 ) -> impl Future<Output = Result<Vec<MemoryRecord>, MemoryError>> + Send {
202 async move {
203 match self {
204 Self::Local(store) => MemoryStore::read(store, ids, keys).await,
205 Self::Remote(client) => MemoryStore::read(client, ids, keys).await,
206 }
207 }
208 }
209 fn list(&self) -> impl Future<Output = Result<Vec<MemoryRecord>, MemoryError>> + Send {
210 async move {
211 match self {
212 Self::Local(store) => MemoryStore::list(store).await,
213 Self::Remote(client) => MemoryStore::list(client).await,
214 }
215 }
216 }
217 fn put(
218 &self,
219 content: &str,
220 replacement: Option<MemoryKey>,
221 ) -> impl Future<Output = Result<MemoryRecord, MemoryError>> + Send {
222 async move {
223 reject_unsafe(content)?;
224 match self {
225 Self::Local(store) => MemoryStore::put(store, content, replacement).await,
226 Self::Remote(client) => MemoryStore::put(client, content, replacement).await,
227 }
228 }
229 }
230 fn delete(&self, key: MemoryKey) -> impl Future<Output = Result<(), MemoryError>> + Send {
231 async move {
232 match self {
233 Self::Local(store) => MemoryStore::delete(store, key).await,
234 Self::Remote(client) => MemoryStore::delete(client, key).await,
235 }
236 }
237 }
238 fn sync(
239 &self,
240 memories: &[MemoryRecord],
241 ) -> impl Future<Output = Result<SyncReport, MemoryError>> + Send {
242 async move {
243 for memory in memories {
244 reject_unsafe(&memory.content)?;
245 }
246 match self {
247 Self::Local(store) => MemoryStore::sync(store, memories).await,
248 Self::Remote(client) => MemoryStore::sync(client, memories).await,
249 }
250 }
251 }
252 fn export_page(
253 &self,
254 namespaces: Option<&[String]>,
255 cursor: Option<&ExportCursor>,
256 limit: usize,
257 ) -> impl Future<Output = Result<(Vec<MemoryRecord>, Option<ExportCursor>), MemoryError>> + Send
258 {
259 async move {
260 match self {
261 Self::Local(store) => {
262 MemoryStore::export_page(store, namespaces, cursor, limit).await
263 }
264 Self::Remote(client) => {
265 MemoryStore::export_page(client, namespaces, cursor, limit).await
266 }
267 }
268 }
269 }
270}
271
272#[cfg(all(feature = "client", feature = "local"))]
273fn reject_unsafe(content: &str) -> Result<(), MemoryError> {
274 if contains_likely_secret(content) {
275 return Err(MemoryError::SecretRejected);
276 }
277 Ok(())
278}
279
280#[cfg(feature = "local")]
281fn current_time_ms() -> i64 {
282 let milliseconds = SystemTime::now()
283 .duration_since(UNIX_EPOCH)
284 .unwrap_or_default()
285 .as_millis();
286 i64::try_from(milliseconds).unwrap_or(i64::MAX)
287}
288
289#[derive(Debug, Error)]
291pub enum MemoryError {
292 #[error("memory content is empty")]
294 EmptyContent,
295 #[error("memory content exceeds the {maximum_bytes}-byte limit")]
297 ContentTooLarge {
298 maximum_bytes: usize,
300 },
301 #[error("memory query exceeds the {maximum_bytes}-byte limit")]
303 QueryTooLarge {
304 maximum_bytes: usize,
306 },
307 #[error("memory record capacity of {maximum} was reached")]
309 RecordCapacity {
310 maximum: usize,
312 },
313 #[error("memory content capacity of {maximum_bytes} bytes was reached")]
315 ContentCapacity {
316 maximum_bytes: usize,
318 },
319 #[error("memory storage capacity was reached")]
321 StorageCapacity,
322 #[error("memory content was rejected as a likely secret")]
324 SecretRejected,
325 #[error("an equivalent memory already exists")]
327 Duplicate,
328 #[error("memory was not found")]
330 NotFound,
331 #[error("memory changed since it was read")]
333 Conflict,
334 #[error("memories from other namespaces are read-only")]
336 RemoteReadOnly,
337 #[error("memory store returned invalid pagination")]
339 InvalidPagination,
340 #[error(
342 "memory schema version {found} is unsupported; this build supports version {supported}"
343 )]
344 UnsupportedSchemaVersion {
345 found: i64,
347 supported: i64,
349 },
350 #[error("memory backend operation failed")]
352 Backend {
353 #[source]
355 source: Box<dyn Error + Send + Sync>,
356 },
357 #[error("memory backend is temporarily unavailable")]
359 Unavailable {
360 #[source]
362 source: Box<dyn Error + Send + Sync>,
363 },
364}
365
366impl MemoryError {
367 pub fn backend(source: impl Error + Send + Sync + 'static) -> Self {
369 Self::Backend {
370 source: Box::new(source),
371 }
372 }
373
374 pub fn unavailable(source: impl Error + Send + Sync + 'static) -> Self {
376 Self::Unavailable {
377 source: Box::new(source),
378 }
379 }
380
381 pub fn is_retryable(&self) -> bool {
383 matches!(self, Self::Unavailable { .. })
384 }
385}
386
387#[cfg(feature = "client")]
388impl From<RemoteClientError> for MemoryError {
389 fn from(source: RemoteClientError) -> Self {
390 match source {
391 error @ (RemoteClientError::Transport | RemoteClientError::Unavailable) => {
392 Self::unavailable(error)
393 }
394 RemoteClientError::ReadOnly | RemoteClientError::NamespaceMismatch => {
395 Self::RemoteReadOnly
396 }
397 RemoteClientError::Rejected { code } => match code {
398 protocol::RemoteErrorCode::QueryTooLarge => Self::QueryTooLarge {
399 maximum_bytes: MemoryLimits::PRODUCTION.query_bytes,
400 },
401 protocol::RemoteErrorCode::ContentTooLarge => Self::ContentTooLarge {
402 maximum_bytes: MemoryLimits::PRODUCTION.content_bytes,
403 },
404 protocol::RemoteErrorCode::RecordCapacity => Self::RecordCapacity {
405 maximum: MemoryLimits::PRODUCTION.records,
406 },
407 protocol::RemoteErrorCode::ContentCapacity => Self::ContentCapacity {
408 maximum_bytes: MemoryLimits::PRODUCTION.total_content_bytes,
409 },
410 protocol::RemoteErrorCode::Duplicate => Self::Duplicate,
411 protocol::RemoteErrorCode::NotFound => Self::NotFound,
412 protocol::RemoteErrorCode::Conflict => Self::Conflict,
413 protocol::RemoteErrorCode::Forbidden
414 | protocol::RemoteErrorCode::NamespaceMismatch => Self::RemoteReadOnly,
415 protocol::RemoteErrorCode::Unavailable => {
416 Self::unavailable(RemoteClientError::Rejected { code })
417 }
418 _ => Self::backend(RemoteClientError::Rejected { code }),
419 },
420 error => Self::backend(error),
421 }
422 }
423}