1use crate::error::LaserError;
2use crate::kv::{
3 AGDX_KV_CAS_CODE, AGDX_KV_CAS_FENCED_CODE, AGDX_KV_COPY_CODE, AGDX_KV_DELETE_CODE,
4 AGDX_KV_DELETE_MANY_CODE, AGDX_KV_EXISTS_CODE, AGDX_KV_EXPIRE_CODE, AGDX_KV_GET_CODE,
5 AGDX_KV_LEASE_RENEW_CODE, AGDX_KV_MOVE_CODE, AGDX_KV_NAMESPACES_CODE, AGDX_KV_PATCH_CODE,
6 AGDX_KV_RELEASE_CODE, AGDX_KV_SCAN_CODE, AGDX_KV_SET_CODE, CasExpect, DEFAULT_SCAN_LIMIT,
7 KV_LEASE_OP_VERSION, KV_OP_VERSION, KvCasFenced, KvEntry, KvError, KvGet, KvLease,
8 KvLeaseRenew, KvMetadata, KvNamespaceInfo, KvPage, KvRelease, MAX_KEY_BYTES, MAX_VALUE_BYTES,
9};
10use crate::laser::Laser;
11use crate::stream::{Codec, Decoder};
12use crate::types::ConversationId;
13use laser_wire::framing::encode_named;
14use laser_wire::kv::{
15 KvCas, KvCopy, KvDelete, KvDeleteMany, KvExists, KvExpire, KvNamespaces, KvOutcome, KvPatch,
16 KvReply, KvScan, KvSet,
17};
18use laser_wire::mutation::MutationPosition;
19use laser_wire::validate::Validate;
20use serde::Serialize;
21use serde::de::DeserializeOwned;
22use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Lease {
31 pub token: u64,
32 pub granted_ttl: Duration,
33 pub position: MutationPosition,
34}
35
36impl Laser {
37 pub fn kv(&self, namespace: impl Into<String>) -> Kv {
53 let namespace = self.resource_name(&namespace.into());
54 Kv {
55 laser: self.clone(),
56 namespace,
57 session: None,
58 }
59 }
60
61 pub async fn kv_namespaces(&self) -> Result<Vec<KvNamespaceInfo>, LaserError> {
67 let request = KvNamespaces { v: KV_OP_VERSION };
68 match self
69 .execute_kv(None, AGDX_KV_NAMESPACES_CODE, &request)
70 .await?
71 {
72 KvOutcome::Namespaces(namespaces) => Ok(self.local_namespaces(namespaces)),
73 _ => Err(LaserError::Protocol(
74 "kv namespaces: unexpected outcome".to_owned(),
75 )),
76 }
77 }
78
79 fn local_namespaces(&self, namespaces: Vec<KvNamespaceInfo>) -> Vec<KvNamespaceInfo> {
80 namespaces
81 .into_iter()
82 .filter_map(|mut info| {
83 let local = self.local_resource_name(&info.namespace)?.to_owned();
84 info.namespace = local;
85 Some(info)
86 })
87 .collect()
88 }
89
90 pub(crate) async fn execute_kv(
93 &self,
94 namespace: Option<&str>,
95 code: u32,
96 request: &impl Serialize,
97 ) -> Result<KvOutcome, LaserError> {
98 if let Some(namespace) = namespace {
99 laser_wire::kv::validate_namespace(namespace)?;
100 }
101 let capabilities = self.capabilities().await;
102 if !capabilities.kv.available {
103 return Err(LaserError::unsupported(
104 "kv",
105 "the key-value surface is not served by this deployment",
106 ));
107 }
108 if let Some(versions) = capabilities.versions
111 && versions.kv != KV_OP_VERSION
112 {
113 return Err(KvError::Version {
114 expected: versions.kv,
115 got: KV_OP_VERSION,
116 }
117 .into());
118 }
119 let payload = encode_named(request)
120 .map_err(|error| LaserError::Codec(format!("encode request: {error}")))?;
121 let payload = self.send_raw_with_response(code, payload).await?;
122 match crate::error::decode_managed_reply::<KvReply>(&payload)? {
123 KvReply::Ok(outcome) => Ok(outcome),
124 KvReply::Err(error) => Err(error.into()),
125 _ => Err(LaserError::Protocol("kv: unknown reply variant".to_owned())),
126 }
127 }
128}
129
130pub struct Kv {
133 laser: Laser,
134 namespace: String,
135 session: Option<laser_wire::agent::SessionRef>,
136}
137
138impl Kv {
139 #[must_use]
142 pub fn in_session(mut self, session: laser_wire::agent::SessionRef) -> Self {
143 self.session = Some(session);
144 self
145 }
146
147 pub fn session(&self) -> Option<&laser_wire::agent::SessionRef> {
149 self.session.as_ref()
150 }
151
152 pub fn namespace(&self) -> &str {
154 self.laser
155 .local_resource_name(&self.namespace)
156 .unwrap_or(&self.namespace)
157 }
158
159 pub fn resource_namespace(&self) -> &str {
162 &self.namespace
163 }
164
165 pub async fn get(&self, key: impl AsRef<[u8]>) -> Result<Option<Vec<u8>>, LaserError> {
168 Ok(self.get_entry(key).await?.map(|entry| entry.value))
169 }
170
171 pub async fn get_entry(&self, key: impl AsRef<[u8]>) -> Result<Option<KvEntry>, LaserError> {
173 let key = validated_key(key.as_ref())?;
174 let request = KvGet {
175 v: KV_OP_VERSION,
176 namespace: self.namespace.clone(),
177 key,
178 if_none_match: None,
179 min_position: None,
180 };
181 match self
182 .laser
183 .execute_kv(Some(&self.namespace), AGDX_KV_GET_CODE, &request)
184 .await?
185 {
186 KvOutcome::Value(entry) => Ok(entry),
187 other => Err(unexpected("get", &other)),
188 }
189 }
190
191 pub async fn get_entry_at_least(
200 &self,
201 key: impl AsRef<[u8]>,
202 min_position: MutationPosition,
203 ) -> Result<Option<KvEntry>, LaserError> {
204 let key = validated_key(key.as_ref())?;
205 self.require_fenced_leases("get_entry_at_least").await?;
206 let request = KvGet {
207 v: KV_OP_VERSION,
208 namespace: self.namespace.clone(),
209 key,
210 if_none_match: None,
211 min_position: Some(min_position),
212 };
213 match self
214 .laser
215 .execute_kv(Some(&self.namespace), AGDX_KV_GET_CODE, &request)
216 .await?
217 {
218 KvOutcome::Value(entry) => Ok(entry),
219 other => Err(unexpected("get", &other)),
220 }
221 }
222
223 pub async fn get_typed<T: DeserializeOwned>(
226 &self,
227 key: impl AsRef<[u8]>,
228 ) -> Result<Option<T>, LaserError> {
229 self.get_as::<crate::stream::Json, T>(key).await
230 }
231
232 pub async fn get_as<C, T>(&self, key: impl AsRef<[u8]>) -> Result<Option<T>, LaserError>
247 where
248 C: Decoder<T>,
249 {
250 match self.get(key).await? {
251 Some(payload) => C::decode(&payload).map(Some).map_err(LaserError::from),
252 None => Ok(None),
253 }
254 }
255
256 pub fn set(&self, key: impl AsRef<[u8]>) -> KvSetRequest {
260 KvSetRequest {
261 laser: self.laser.clone(),
262 namespace: self.namespace.clone(),
263 session: self.session.clone(),
264 key: key.as_ref().to_vec(),
265 value: Vec::new(),
266 expires_at_micros: None,
267 expect: None,
268 }
269 }
270
271 pub fn cas_fenced(
280 &self,
281 key: impl AsRef<[u8]>,
282 fence_namespace: impl Into<String>,
283 fence_key: impl AsRef<[u8]>,
284 fence_token: u64,
285 ) -> KvCasFencedRequest {
286 KvCasFencedRequest {
287 laser: self.laser.clone(),
288 namespace: self.namespace.clone(),
289 key: key.as_ref().to_vec(),
290 value: Vec::new(),
291 expires_at_micros: None,
292 expect: None,
293 fence_namespace: self.laser.resource_name(&fence_namespace.into()),
294 fence_key: fence_key.as_ref().to_vec(),
295 fence_token,
296 }
297 }
298
299 pub async fn delete(&self, key: impl AsRef<[u8]>) -> Result<bool, LaserError> {
302 let key = validated_key(key.as_ref())?;
303 let request = KvDelete {
304 v: KV_OP_VERSION,
305 namespace: self.namespace.clone(),
306 session: self.session.clone(),
307 key,
308 if_match: None,
309 };
310 match self
311 .laser
312 .execute_kv(Some(&self.namespace), AGDX_KV_DELETE_CODE, &request)
313 .await?
314 {
315 KvOutcome::Deleted(existed) => Ok(existed),
316 other => Err(unexpected("delete", &other)),
317 }
318 }
319
320 pub async fn exists(&self, key: impl AsRef<[u8]>) -> Result<Option<KvMetadata>, LaserError> {
323 let key = validated_key(key.as_ref())?;
324 let request = KvExists {
325 v: KV_OP_VERSION,
326 namespace: self.namespace.clone(),
327 key,
328 };
329 match self
330 .laser
331 .execute_kv(Some(&self.namespace), AGDX_KV_EXISTS_CODE, &request)
332 .await?
333 {
334 KvOutcome::Metadata(metadata) => Ok(metadata),
335 other => Err(unexpected("exists", &other)),
336 }
337 }
338
339 pub async fn expire(
342 &self,
343 key: impl AsRef<[u8]>,
344 ttl: Option<Duration>,
345 ) -> Result<u64, LaserError> {
346 let expires_at_micros = ttl.map(|ttl| now_micros().saturating_add(duration_micros(ttl)));
347 self.expire_at(key, expires_at_micros).await
348 }
349
350 pub async fn expire_at(
354 &self,
355 key: impl AsRef<[u8]>,
356 expires_at_micros: Option<u64>,
357 ) -> Result<u64, LaserError> {
358 let key = validated_key(key.as_ref())?;
359 let request = KvExpire {
360 v: KV_OP_VERSION,
361 namespace: self.namespace.clone(),
362 key,
363 expires_at_micros,
364 };
365 match self
366 .laser
367 .execute_kv(Some(&self.namespace), AGDX_KV_EXPIRE_CODE, &request)
368 .await?
369 {
370 KvOutcome::Versioned { version } => Ok(version),
371 other => Err(unexpected("expire", &other)),
372 }
373 }
374
375 pub async fn patch(
379 &self,
380 key: impl AsRef<[u8]>,
381 patch: impl Into<Vec<u8>>,
382 ) -> Result<u64, LaserError> {
383 let key = validated_key(key.as_ref())?;
384 let request = KvPatch {
385 v: KV_OP_VERSION,
386 namespace: self.namespace.clone(),
387 session: self.session.clone(),
388 key,
389 patch: patch.into(),
390 if_match: None,
391 };
392 match self
393 .laser
394 .execute_kv(Some(&self.namespace), AGDX_KV_PATCH_CODE, &request)
395 .await?
396 {
397 KvOutcome::Versioned { version } => Ok(version),
398 other => Err(unexpected("patch", &other)),
399 }
400 }
401
402 pub async fn lease(
420 &self,
421 key: impl AsRef<[u8]>,
422 holder: impl Into<String>,
423 ttl: Duration,
424 ) -> Result<Lease, LaserError> {
425 let key = validated_key(key.as_ref())?;
426 self.require_fenced_leases("lease").await?;
427 let request = KvLease {
428 v: KV_LEASE_OP_VERSION,
429 namespace: self.namespace.clone(),
430 key,
431 lease_ttl_micros: duration_micros(ttl),
432 holder_id: holder.into(),
433 subject_user_id: None,
434 };
435 request.validate()?;
436 self.laser.acquire_lease(request).await
437 }
438
439 pub async fn renew_lease(
447 &self,
448 key: impl AsRef<[u8]>,
449 holder: impl Into<String>,
450 token: u64,
451 ttl: Duration,
452 ) -> Result<Lease, LaserError> {
453 let key = validated_key(key.as_ref())?;
454 self.require_fenced_leases("renew_lease").await?;
455 let request = KvLeaseRenew {
456 v: KV_LEASE_OP_VERSION,
457 namespace: self.namespace.clone(),
458 key,
459 holder_id: holder.into(),
460 subject_user_id: None,
461 lease_token: token,
462 lease_ttl_micros: duration_micros(ttl),
463 };
464 request.validate()?;
465 match self
466 .laser
467 .execute_kv(Some(&self.namespace), AGDX_KV_LEASE_RENEW_CODE, &request)
468 .await?
469 {
470 KvOutcome::Renewed {
471 lease_token,
472 granted_ttl_micros,
473 position,
474 } => Ok(Lease {
475 token: lease_token,
476 granted_ttl: Duration::from_micros(granted_ttl_micros),
477 position,
478 }),
479 other => Err(unexpected("renew_lease", &other)),
480 }
481 }
482
483 pub async fn release(
488 &self,
489 key: impl AsRef<[u8]>,
490 holder: impl Into<String>,
491 token: u64,
492 ) -> Result<bool, LaserError> {
493 let key = validated_key(key.as_ref())?;
494 self.require_fenced_leases("release").await?;
495 let request = KvRelease {
496 v: KV_LEASE_OP_VERSION,
497 namespace: self.namespace.clone(),
498 key,
499 lease_token: token,
500 holder_id: holder.into(),
501 };
502 request.validate()?;
503 match self
504 .laser
505 .execute_kv(Some(&request.namespace), AGDX_KV_RELEASE_CODE, &request)
506 .await?
507 {
508 KvOutcome::Released(released) => Ok(released),
509 other => Err(unexpected("release", &other)),
510 }
511 }
512
513 async fn require_fenced_leases(&self, what: &'static str) -> Result<(), LaserError> {
517 if self.laser.capabilities().await.kv.fenced_leases {
518 Ok(())
519 } else {
520 Err(LaserError::unsupported_feature(
521 "kv",
522 what,
523 "the fenced-lease contract is not advertised by this deployment",
524 ))
525 }
526 }
527
528 pub fn copy_to(&self, key: impl AsRef<[u8]>, to_key: impl AsRef<[u8]>) -> KvCopyRequest {
534 KvCopyRequest {
535 laser: self.laser.clone(),
536 namespace: self.namespace.clone(),
537 key: key.as_ref().to_vec(),
538 to_namespace: None,
539 to_key: to_key.as_ref().to_vec(),
540 delete_source: false,
541 }
542 }
543
544 pub fn move_to(&self, key: impl AsRef<[u8]>, to_key: impl AsRef<[u8]>) -> KvCopyRequest {
547 KvCopyRequest {
548 delete_source: true,
549 ..self.copy_to(key, to_key)
550 }
551 }
552
553 pub async fn get_many(
557 &self,
558 keys: impl IntoIterator<Item = impl AsRef<[u8]>>,
559 ) -> Result<Vec<Option<Vec<u8>>>, LaserError> {
560 let mut ops = Vec::new();
561 for key in keys {
562 let request = KvGet {
563 v: KV_OP_VERSION,
564 namespace: self.namespace.clone(),
565 key: validated_key(key.as_ref())?,
566 if_none_match: None,
567 min_position: None,
568 };
569 ops.push(laser_wire::batch::BatchItem {
570 code: AGDX_KV_GET_CODE,
571 payload: encode_named(&request)
572 .map_err(|error| LaserError::Codec(format!("encode get: {error}")))?,
573 });
574 }
575 let results = self.laser.execute_batch(ops).await?;
576 results
577 .iter()
578 .map(
579 |slot| match crate::error::decode_managed_reply::<KvReply>(slot)? {
580 KvReply::Ok(KvOutcome::Value(entry)) => Ok(entry.map(|entry| entry.value)),
581 KvReply::Ok(ref other) => Err(unexpected("get", other)),
582 KvReply::Err(error) => Err(error.into()),
583 _ => Err(LaserError::Protocol(
586 "unrecognized kv reply in a batch slot".to_owned(),
587 )),
588 },
589 )
590 .collect()
591 }
592
593 pub fn delete_many(&self) -> KvDeleteManyRequest {
604 KvDeleteManyRequest {
605 laser: self.laser.clone(),
606 namespace: self.namespace.clone(),
607 prefix: None,
608 start: None,
609 end: None,
610 key_contains: None,
611 conversation: None,
612 stream: None,
613 }
614 }
615
616 pub fn scan(&self) -> KvScanRequest {
619 KvScanRequest {
620 laser: self.laser.clone(),
621 namespace: self.namespace.clone(),
622 prefix: None,
623 start: None,
624 end: None,
625 key_contains: None,
626 conversation: None,
627 stream: None,
628 limit: DEFAULT_SCAN_LIMIT,
629 cursor: None,
630 }
631 }
632}
633
634#[cfg(feature = "agent")]
643impl crate::state_store::StateStore for Kv {
644 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError> {
645 Kv::get(self, key).await
646 }
647
648 async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), LaserError> {
649 Kv::set(self, key).bytes(value).send().await
650 }
651
652 async fn delete(&self, key: &str) -> Result<(), LaserError> {
653 Kv::delete(self, key).await.map(|_existed| ())
654 }
655}
656
657#[must_use = "call .send().await to write, or .commit().await for a compare-and-swap"]
663pub struct KvSetRequest {
664 laser: Laser,
665 namespace: String,
666 session: Option<laser_wire::agent::SessionRef>,
667 key: Vec<u8>,
668 value: Vec<u8>,
669 expires_at_micros: Option<u64>,
670 expect: Option<CasExpect>,
671}
672
673impl KvSetRequest {
674 pub fn bytes(mut self, payload: impl AsRef<[u8]>) -> Self {
678 self.value = payload.as_ref().to_vec();
679 self
680 }
681
682 pub fn encode_with<C, T>(mut self, value: &T) -> Result<Self, LaserError>
685 where
686 C: Codec<T>,
687 T: ?Sized,
688 {
689 self.value = C::encode(value)?;
690 Ok(self)
691 }
692
693 pub fn json<T: Serialize + ?Sized>(self, value: &T) -> Result<Self, LaserError> {
696 self.encode_with::<crate::stream::Json, T>(value)
697 }
698
699 pub fn msgpack<T: Serialize + ?Sized>(self, value: &T) -> Result<Self, LaserError> {
702 self.encode_with::<crate::stream::Msgpack, T>(value)
703 }
704
705 pub fn ttl(mut self, ttl: Duration) -> Self {
707 self.expires_at_micros = Some(now_micros().saturating_add(duration_micros(ttl)));
708 self
709 }
710
711 pub fn expires_at(mut self, epoch_micros: u64) -> Self {
713 self.expires_at_micros = Some(epoch_micros);
714 self
715 }
716
717 pub fn expect_version(mut self, version: u64) -> Self {
731 self.expect = Some(CasExpect::Match(version));
732 self
733 }
734
735 pub fn expect_absent(mut self) -> Self {
739 self.expect = Some(CasExpect::Absent);
740 self
741 }
742
743 pub async fn commit(self) -> Result<u64, LaserError> {
752 let Some(expect) = self.expect else {
753 return Err(LaserError::Invalid(
754 "commit() needs a precondition: call expect_version(..) or expect_absent()"
755 .to_owned(),
756 ));
757 };
758 if !self.laser.capabilities().await.kv.cas {
763 return Err(LaserError::unsupported_feature(
764 "kv",
765 "cas",
766 "compare-and-swap is not advertised by this deployment",
767 ));
768 }
769 let key = validated_key(&self.key)?;
770 validated_value(&self.value)?;
771 let request = KvCas {
772 v: KV_OP_VERSION,
773 namespace: self.namespace,
774 session: self.session,
775 key,
776 value: self.value,
777 expires_at_micros: self.expires_at_micros,
778 expect,
779 };
780 match self
781 .laser
782 .execute_kv(Some(&request.namespace), AGDX_KV_CAS_CODE, &request)
783 .await?
784 {
785 KvOutcome::Committed { version } => Ok(version),
786 other => Err(unexpected("cas", &other)),
787 }
788 }
789
790 pub async fn send(self) -> Result<(), LaserError> {
795 if self.expect.is_some() {
796 return Err(LaserError::Invalid(
797 "a precondition was set: call commit() instead of send()".to_owned(),
798 ));
799 }
800 let key = validated_key(&self.key)?;
801 validated_value(&self.value)?;
802 let request = KvSet {
803 v: KV_OP_VERSION,
804 namespace: self.namespace,
805 session: self.session,
806 key,
807 value: self.value,
808 expires_at_micros: self.expires_at_micros,
809 };
810 match self
811 .laser
812 .execute_kv(Some(&request.namespace), AGDX_KV_SET_CODE, &request)
813 .await?
814 {
815 KvOutcome::Written => Ok(()),
816 other => Err(unexpected("set", &other)),
817 }
818 }
819}
820
821#[must_use = "call .commit().await to apply the fenced compare-and-swap"]
826pub struct KvCasFencedRequest {
827 laser: Laser,
828 namespace: String,
829 key: Vec<u8>,
830 value: Vec<u8>,
831 expires_at_micros: Option<u64>,
832 expect: Option<CasExpect>,
833 fence_namespace: String,
834 fence_key: Vec<u8>,
835 fence_token: u64,
836}
837
838impl KvCasFencedRequest {
839 pub fn bytes(mut self, payload: impl AsRef<[u8]>) -> Self {
841 self.value = payload.as_ref().to_vec();
842 self
843 }
844
845 pub fn encode_with<C, T>(mut self, value: &T) -> Result<Self, LaserError>
847 where
848 C: Codec<T>,
849 T: ?Sized,
850 {
851 self.value = C::encode(value)?;
852 Ok(self)
853 }
854
855 pub fn json<T: Serialize + ?Sized>(self, value: &T) -> Result<Self, LaserError> {
857 self.encode_with::<crate::stream::Json, T>(value)
858 }
859
860 pub fn msgpack<T: Serialize + ?Sized>(self, value: &T) -> Result<Self, LaserError> {
862 self.encode_with::<crate::stream::Msgpack, T>(value)
863 }
864
865 pub fn ttl(mut self, ttl: Duration) -> Self {
867 self.expires_at_micros = Some(now_micros().saturating_add(duration_micros(ttl)));
868 self
869 }
870
871 pub fn expires_at(mut self, epoch_micros: u64) -> Self {
873 self.expires_at_micros = Some(epoch_micros);
874 self
875 }
876
877 pub fn expect_version(mut self, version: u64) -> Self {
879 self.expect = Some(CasExpect::Match(version));
880 self
881 }
882
883 pub fn expect_absent(mut self) -> Self {
885 self.expect = Some(CasExpect::Absent);
886 self
887 }
888
889 pub async fn commit(self) -> Result<u64, LaserError> {
899 let Some(expect) = self.expect else {
900 return Err(LaserError::Invalid(
901 "commit() needs a precondition: call expect_version(..) or expect_absent()"
902 .to_owned(),
903 ));
904 };
905 if !self.laser.capabilities().await.kv.fenced_leases {
909 return Err(LaserError::unsupported_feature(
910 "kv",
911 "cas_fenced",
912 "the fenced-lease contract is not advertised by this deployment",
913 ));
914 }
915 let key = validated_key(&self.key)?;
916 let fence_key = validated_key(&self.fence_key)?;
917 validated_value(&self.value)?;
918 let request = KvCasFenced {
919 v: KV_LEASE_OP_VERSION,
920 namespace: self.namespace,
921 key,
922 value: self.value,
923 expires_at_micros: self.expires_at_micros,
924 expect,
925 fence_namespace: self.fence_namespace,
926 fence_key,
927 fence_token: self.fence_token,
928 };
929 request.validate()?;
930 match self
931 .laser
932 .execute_kv(Some(&request.namespace), AGDX_KV_CAS_FENCED_CODE, &request)
933 .await?
934 {
935 KvOutcome::Committed { version } => Ok(version),
936 other => Err(unexpected("cas_fenced", &other)),
937 }
938 }
939}
940
941#[must_use = "call .fetch().await or .entries().await to read the page"]
944pub struct KvScanRequest {
945 laser: Laser,
946 namespace: String,
947 prefix: Option<Vec<u8>>,
948 start: Option<Vec<u8>>,
949 end: Option<Vec<u8>>,
950 key_contains: Option<String>,
951 conversation: Option<String>,
952 stream: Option<String>,
953 limit: usize,
954 cursor: Option<Vec<u8>>,
955}
956
957impl KvScanRequest {
958 pub fn conversation(mut self, conversation: ConversationId) -> Self {
963 self.conversation = Some(conversation.to_string());
964 self.stream = self.laser.resource_stream().map(str::to_owned);
965 self
966 }
967
968 pub fn prefix(mut self, prefix: impl AsRef<[u8]>) -> Self {
971 self.prefix = Some(prefix.as_ref().to_vec());
972 self
973 }
974
975 pub fn range(mut self, start: impl AsRef<[u8]>, end: impl AsRef<[u8]>) -> Self {
977 self.start = Some(start.as_ref().to_vec());
978 self.end = Some(end.as_ref().to_vec());
979 self
980 }
981
982 pub fn key_contains(mut self, substring: impl Into<String>) -> Self {
985 self.key_contains = Some(substring.into());
986 self
987 }
988
989 pub fn limit(mut self, n: usize) -> Self {
991 self.limit = n.min(laser_wire::limits::MAX_SCAN_LIMIT);
992 self
993 }
994
995 pub fn cursor(mut self, cursor: impl AsRef<[u8]>) -> Self {
997 self.cursor = Some(cursor.as_ref().to_vec());
998 self
999 }
1000
1001 pub async fn fetch(self) -> Result<KvPage, LaserError> {
1003 let request = self.request();
1004 match self
1005 .laser
1006 .execute_kv(Some(&self.namespace), AGDX_KV_SCAN_CODE, &request)
1007 .await?
1008 {
1009 KvOutcome::Page(page) => Ok(page),
1010 other => Err(unexpected("scan", &other)),
1011 }
1012 }
1013
1014 pub async fn entries(self) -> Result<Vec<KvEntry>, LaserError> {
1017 let laser = self.laser.clone();
1018 let mut request = self.request();
1019 let mut out = Vec::new();
1020 loop {
1021 let page = match laser
1022 .execute_kv(Some(&request.namespace), AGDX_KV_SCAN_CODE, &request)
1023 .await?
1024 {
1025 KvOutcome::Page(page) => page,
1026 other => return Err(unexpected("scan", &other)),
1027 };
1028 out.extend(page.entries);
1029 match page.cursor {
1030 Some(cursor) => request.cursor = Some(cursor),
1031 None => return Ok(out),
1032 }
1033 }
1034 }
1035
1036 #[cfg(feature = "agent")]
1039 pub(crate) fn lens_stream(mut self, stream: Option<String>) -> Self {
1040 self.stream = stream;
1041 self
1042 }
1043
1044 fn request(&self) -> KvScan {
1045 KvScan {
1046 v: KV_OP_VERSION,
1047 namespace: self.namespace.clone(),
1048 prefix: self.prefix.clone(),
1049 start: self.start.clone(),
1050 end: self.end.clone(),
1051 key_contains: self.key_contains.clone(),
1052 conversation: self.conversation.clone(),
1053 stream: self.stream.clone(),
1054 limit: self.limit,
1055 cursor: self.cursor.clone(),
1056 }
1057 }
1058}
1059
1060#[must_use = "call .send().await to delete the matched entries"]
1063pub struct KvDeleteManyRequest {
1064 laser: Laser,
1065 namespace: String,
1066 prefix: Option<Vec<u8>>,
1067 start: Option<Vec<u8>>,
1068 end: Option<Vec<u8>>,
1069 key_contains: Option<String>,
1070 conversation: Option<String>,
1071 stream: Option<String>,
1072}
1073
1074impl KvDeleteManyRequest {
1075 pub fn conversation(mut self, conversation: ConversationId) -> Self {
1078 self.conversation = Some(conversation.to_string());
1079 self.stream = self.laser.resource_stream().map(str::to_owned);
1080 self
1081 }
1082
1083 pub fn prefix(mut self, prefix: impl AsRef<[u8]>) -> Self {
1085 self.prefix = Some(prefix.as_ref().to_vec());
1086 self
1087 }
1088
1089 pub fn range(mut self, start: impl AsRef<[u8]>, end: impl AsRef<[u8]>) -> Self {
1091 self.start = Some(start.as_ref().to_vec());
1092 self.end = Some(end.as_ref().to_vec());
1093 self
1094 }
1095
1096 pub fn key_contains(mut self, substring: impl Into<String>) -> Self {
1099 self.key_contains = Some(substring.into());
1100 self
1101 }
1102
1103 pub async fn send(self) -> Result<usize, LaserError> {
1105 let request = KvDeleteMany {
1106 v: KV_OP_VERSION,
1107 namespace: self.namespace,
1108 prefix: self.prefix,
1109 start: self.start,
1110 end: self.end,
1111 key_contains: self.key_contains,
1112 conversation: self.conversation,
1113 stream: self.stream,
1114 };
1115 match self
1116 .laser
1117 .execute_kv(Some(&request.namespace), AGDX_KV_DELETE_MANY_CODE, &request)
1118 .await?
1119 {
1120 KvOutcome::DeletedMany(removed) => Ok(removed),
1121 other => Err(unexpected("delete_many", &other)),
1122 }
1123 }
1124}
1125
1126#[must_use = "call .send().await to copy or move the value"]
1130pub struct KvCopyRequest {
1131 laser: Laser,
1132 namespace: String,
1133 key: Vec<u8>,
1134 to_namespace: Option<String>,
1135 to_key: Vec<u8>,
1136 delete_source: bool,
1137}
1138
1139impl KvCopyRequest {
1140 pub fn into_namespace(mut self, namespace: impl Into<String>) -> Self {
1142 self.to_namespace = Some(self.laser.resource_name(&namespace.into()));
1143 self
1144 }
1145
1146 pub async fn send(self) -> Result<u64, LaserError> {
1148 let key = validated_key(&self.key)?;
1149 let to_key = validated_key(&self.to_key)?;
1150 let code = if self.delete_source {
1151 AGDX_KV_MOVE_CODE
1152 } else {
1153 AGDX_KV_COPY_CODE
1154 };
1155 if let Some(to_namespace) = &self.to_namespace {
1156 laser_wire::kv::validate_namespace(to_namespace)?;
1157 }
1158 let request = KvCopy {
1160 v: KV_OP_VERSION,
1161 namespace: self.namespace,
1162 key,
1163 to_namespace: self.to_namespace,
1164 to_key,
1165 };
1166 match self
1167 .laser
1168 .execute_kv(Some(&request.namespace), code, &request)
1169 .await?
1170 {
1171 KvOutcome::Committed { version } => Ok(version),
1172 other => Err(unexpected("copy", &other)),
1173 }
1174 }
1175}
1176
1177fn validated_key(key: &[u8]) -> Result<Vec<u8>, LaserError> {
1180 if key.is_empty() {
1181 return Err(LaserError::Invalid("key must not be empty".to_owned()));
1182 }
1183 if key.len() > MAX_KEY_BYTES {
1184 return Err(LaserError::Invalid(format!(
1185 "key is {}B, exceeds cap {MAX_KEY_BYTES}B",
1186 key.len()
1187 )));
1188 }
1189 Ok(key.to_vec())
1190}
1191
1192fn validated_value(value: &[u8]) -> Result<(), LaserError> {
1196 if value.len() > MAX_VALUE_BYTES {
1197 return Err(LaserError::Invalid(format!(
1198 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
1199 value.len()
1200 )));
1201 }
1202 Ok(())
1203}
1204
1205fn unexpected(op: &str, outcome: &KvOutcome) -> LaserError {
1206 LaserError::Protocol(format!("kv {op}: unexpected reply outcome {outcome:?}"))
1207}
1208
1209fn now_micros() -> u64 {
1210 SystemTime::now()
1211 .duration_since(UNIX_EPOCH)
1212 .map(duration_micros)
1213 .unwrap_or(0)
1214}
1215
1216fn duration_micros(duration: Duration) -> u64 {
1217 u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223 use crate::capabilities::Capabilities;
1224
1225 fn fenced_laser() -> Laser {
1226 let mut capabilities = Capabilities::OPEN;
1227 capabilities.kv.available = true;
1228 capabilities.kv.fenced_leases = true;
1229 Laser::from_client(crate::iggy::prelude::IggyClient::default())
1230 .with_capabilities(capabilities)
1231 }
1232
1233 fn scoped(naming: crate::laser::ResourceNaming) -> Laser {
1234 fenced_laser()
1235 .with_default_stream("acme")
1236 .with_resource_naming(naming)
1237 }
1238
1239 #[test]
1240 fn given_a_default_stream_when_opening_a_namespace_then_should_send_it_scoped() {
1241 let kv = scoped(crate::laser::ResourceNaming::Stream).kv("sessions");
1242 assert_eq!(kv.resource_namespace(), "stream:acme/sessions");
1243 assert_eq!(kv.namespace(), "sessions");
1244 }
1245
1246 #[test]
1247 fn given_bare_naming_when_opening_a_namespace_then_should_send_it_unchanged() {
1248 let kv = scoped(crate::laser::ResourceNaming::Bare).kv("sessions");
1249 assert_eq!(kv.resource_namespace(), "sessions");
1250 assert_eq!(kv.namespace(), "sessions");
1251 }
1252
1253 #[test]
1254 fn given_a_conversation_lens_when_scanning_then_should_name_the_stream() {
1255 let conversation = ConversationId::derive("lens");
1256 let laser = scoped(crate::laser::ResourceNaming::Stream);
1257 let request = laser
1258 .kv("memory")
1259 .scan()
1260 .conversation(conversation)
1261 .request();
1262 assert_eq!(request.namespace, "stream:acme/memory");
1263 assert_eq!(request.stream.as_deref(), Some("acme"));
1264 assert_eq!(request.conversation, Some(conversation.to_string()));
1265 let unfiltered = laser.kv("memory").scan().request();
1266 assert_eq!(unfiltered.stream, None);
1267 let bare = scoped(crate::laser::ResourceNaming::Bare)
1268 .kv("memory")
1269 .scan()
1270 .conversation(conversation)
1271 .request();
1272 assert_eq!(bare.stream, None);
1273 }
1274
1275 #[test]
1276 fn given_a_conversation_lens_when_deleting_many_then_should_name_the_stream() {
1277 let request = scoped(crate::laser::ResourceNaming::Stream)
1278 .kv("memory")
1279 .delete_many()
1280 .conversation(ConversationId::derive("lens"));
1281 assert_eq!(request.namespace, "stream:acme/memory");
1282 assert_eq!(request.stream.as_deref(), Some("acme"));
1283 }
1284
1285 #[test]
1286 fn given_fence_and_copy_namespaces_when_built_then_should_scope_both() {
1287 let kv = scoped(crate::laser::ResourceNaming::Stream).kv("state");
1288 let fenced = kv.cas_fenced("key", "coordination", "lease", 1);
1289 assert_eq!(fenced.namespace, "stream:acme/state");
1290 assert_eq!(fenced.fence_namespace, "stream:acme/coordination");
1291 let copy = kv.copy_to("a", "b").into_namespace("archive");
1292 assert_eq!(copy.to_namespace.as_deref(), Some("stream:acme/archive"));
1293 }
1294
1295 #[test]
1296 fn given_listed_namespaces_when_localized_then_should_keep_only_the_own_stream() {
1297 let info = |namespace: &str| KvNamespaceInfo {
1298 namespace: namespace.to_owned(),
1299 entries: 1,
1300 };
1301 let listed = vec![
1302 info("stream:acme/sessions"),
1303 info("stream:other/sessions"),
1304 info("legacy"),
1305 ];
1306 let local = scoped(crate::laser::ResourceNaming::Stream).local_namespaces(listed.clone());
1307 assert_eq!(
1308 local
1309 .iter()
1310 .map(|info| info.namespace.as_str())
1311 .collect::<Vec<_>>(),
1312 ["sessions"]
1313 );
1314 let bare = scoped(crate::laser::ResourceNaming::Bare).local_namespaces(listed);
1315 assert_eq!(bare.len(), 3);
1316 }
1317
1318 #[test]
1319 fn given_an_empty_key_when_validated_then_should_error() {
1320 assert!(matches!(validated_key(b""), Err(LaserError::Invalid(_))));
1321 }
1322
1323 #[test]
1324 fn given_an_oversized_key_when_validated_then_should_error() {
1325 let key = vec![b'x'; MAX_KEY_BYTES + 1];
1326 assert!(matches!(validated_key(&key), Err(LaserError::Invalid(_))));
1327 }
1328
1329 #[test]
1330 fn given_a_binary_key_when_validated_then_should_accept_it() {
1331 assert!(validated_key(&[0xff, 0x00, 0xfe]).is_ok());
1332 }
1333
1334 #[test]
1335 fn given_an_extreme_duration_when_converted_then_should_saturate() {
1336 assert_eq!(duration_micros(Duration::MAX), u64::MAX);
1337 }
1338
1339 #[tokio::test]
1340 async fn given_invalid_lease_inputs_when_called_then_should_reject_before_transport() {
1341 let kv = fenced_laser().kv("coordination");
1342 assert!(matches!(
1343 kv.lease("key", "", Duration::from_secs(1)).await,
1344 Err(LaserError::Invalid(_))
1345 ));
1346 assert!(matches!(
1347 kv.lease("key", "holder", Duration::ZERO).await,
1348 Err(LaserError::Invalid(_))
1349 ));
1350 assert!(matches!(
1351 kv.lease("key", "x".repeat(129), Duration::from_secs(1))
1352 .await,
1353 Err(LaserError::Invalid(_))
1354 ));
1355 }
1356
1357 #[tokio::test]
1358 async fn given_invalid_renew_release_when_called_then_should_reject_before_transport() {
1359 let kv = fenced_laser().kv("coordination");
1360 assert!(matches!(
1361 kv.renew_lease("key", "holder", 1, Duration::ZERO).await,
1362 Err(LaserError::Invalid(_))
1363 ));
1364 assert!(matches!(
1365 kv.renew_lease("key", "holder", 0, Duration::from_secs(1))
1366 .await,
1367 Err(LaserError::Invalid(_))
1368 ));
1369 assert!(matches!(
1370 kv.release("key", "holder", 0).await,
1371 Err(LaserError::Invalid(_))
1372 ));
1373 }
1374
1375 #[tokio::test]
1376 async fn given_an_empty_key_when_expiring_at_then_should_reject_before_transport() {
1377 let kv = fenced_laser().kv("state");
1378 assert!(matches!(
1379 kv.expire_at("", Some(1)).await,
1380 Err(LaserError::Invalid(_))
1381 ));
1382 assert!(matches!(
1383 kv.expire_at("", None).await,
1384 Err(LaserError::Invalid(_))
1385 ));
1386 }
1387
1388 #[tokio::test]
1389 async fn given_a_precondition_when_sending_then_should_reject_before_transport() {
1390 for request in [
1391 fenced_laser()
1392 .kv("state")
1393 .set("key")
1394 .bytes(b"value")
1395 .expect_absent(),
1396 fenced_laser()
1397 .kv("state")
1398 .set("key")
1399 .bytes(b"value")
1400 .expect_version(3),
1401 ] {
1402 assert!(matches!(request.send().await, Err(LaserError::Invalid(_))));
1403 }
1404 }
1405
1406 #[tokio::test]
1407 async fn given_an_invalid_fence_namespace_when_committing_then_should_reject_before_transport()
1408 {
1409 let result = fenced_laser()
1410 .kv("state")
1411 .cas_fenced("key", "", "lease", 1)
1412 .bytes(b"value")
1413 .expect_absent()
1414 .commit()
1415 .await;
1416 assert!(matches!(result, Err(LaserError::Invalid(_))));
1417 }
1418
1419 #[test]
1420 fn given_a_kv_error_when_converted_then_should_nest_the_typed_error() {
1421 let unsupported = LaserError::from(KvError::Unsupported("no managed backend".to_owned()));
1422 assert!(matches!(
1423 unsupported,
1424 LaserError::Kv(KvError::Unsupported(_))
1425 ));
1426 assert!(unsupported.is_unsupported());
1427
1428 let backend = LaserError::from(KvError::Backend("boom".to_owned()));
1429 assert!(matches!(backend, LaserError::Kv(KvError::Backend(_))));
1430 assert!(!backend.is_retryable());
1431
1432 let skew = LaserError::from(KvError::Version {
1433 expected: 1,
1434 got: 2,
1435 });
1436 assert!(matches!(skew, LaserError::Kv(KvError::Version { .. })));
1437 assert!(skew.is_version_skew());
1438 assert!(!skew.is_retryable());
1439 }
1440}