@@ -347,6 +347,223 @@ pub trait KVStore {
347347 ) -> impl Future < Output = Result < Vec < String > , io:: Error > > + ' static + MaybeSend ;
348348}
349349
350+ /// Represents the response from a paginated `list` operation.
351+ ///
352+ /// Contains the list of keys and an optional `next_page_token` that can be used to retrieve the
353+ /// next set of keys.
354+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
355+ pub struct PaginatedListResponse {
356+ /// A vector of keys, ordered in descending order of `creation_time`.
357+ pub keys : Vec < String > ,
358+
359+ /// A token that can be used to retrieve the next set of keys.
360+ /// The `String` is the last key from the current page and the `i64` is
361+ /// the associated `creation_time` used for ordering.
362+ ///
363+ /// Is `None` if there are no more pages to retrieve.
364+ pub next_page_token : Option < ( String , i64 ) > ,
365+ }
366+
367+ /// Provides an interface that allows storage and retrieval of persisted values that are associated
368+ /// with given keys, with support for pagination with time-based ordering.
369+ ///
370+ /// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
371+ /// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
372+ /// ways, as long as per-namespace key uniqueness is asserted.
373+ ///
374+ /// Keys and namespaces are required to be valid ASCII strings in the range of
375+ /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
376+ /// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
377+ /// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
378+ /// should always be separated by primary namespace first, before secondary namespaces are used.
379+ /// While the number of primary namespaces will be relatively small and determined at compile time,
380+ /// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
381+ /// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
382+ /// and equally named primary or secondary namespaces must be avoided.
383+ ///
384+ /// **Note:** This trait extends the functionality of [`KVStoreSync`] by adding support for
385+ /// paginated listing of keys based on a monotonic counter or logical timestamp. This is useful
386+ /// when dealing with a large number of keys that cannot be efficiently retrieved all at once.
387+ ///
388+ /// For an asynchronous version of this trait, see [`PaginatedKVStore`].
389+ pub trait PaginatedKVStoreSync : KVStoreSync {
390+ /// Persists the given data under the given `key`.
391+ ///
392+ /// If the key does not exist, it will be created with the given `creation_time`. If the key
393+ /// already exists, the data will be updated but the original `creation_time` will be preserved.
394+ /// This ensures consistent pagination even when entries are updated.
395+ ///
396+ /// The `creation_time` parameter is an `i64` representing a monotonic counter or logical
397+ /// timestamp. It is used to track the order of keys for list operations, allowing results to
398+ /// be returned in descending order of creation time. Since `creation_time` is immutable after
399+ /// initial creation, pagination remains consistent even when entries are updated.
400+ ///
401+ /// Will create the given `primary_namespace` and `secondary_namespace` if not already present
402+ /// in the store.
403+ fn write_with_time (
404+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , creation_time : i64 ,
405+ buf : Vec < u8 > ,
406+ ) -> Result < ( ) , io:: Error > ;
407+
408+ /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
409+ /// `primary_namespace`, ordered in descending order of `creation_time`.
410+ ///
411+ /// The `list_paginated` method returns the latest records first, based on the `creation_time`
412+ /// associated with each key. Pagination is controlled by the `next_page_token`, which is an
413+ /// `Option<(String, i64)>` used to determine the starting point for the next page of results.
414+ /// If `next_page_token` is `None`, the listing starts from the most recent entry. The
415+ /// `next_page_token` in the returned [`PaginatedListResponse`] can be used to fetch the next
416+ /// page of results.
417+ ///
418+ /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
419+ /// there are no more keys to return.
420+ fn list_paginated (
421+ & self , primary_namespace : & str , secondary_namespace : & str ,
422+ next_page_token : Option < ( String , i64 ) > ,
423+ ) -> Result < PaginatedListResponse , io:: Error > ;
424+ }
425+
426+ /// A wrapper around a [`PaginatedKVStoreSync`] that implements the [`PaginatedKVStore`] trait.
427+ /// It is not necessary to use this type directly.
428+ #[ derive( Clone ) ]
429+ pub struct PaginatedKVStoreSyncWrapper < K : Deref > ( pub K )
430+ where
431+ K :: Target : PaginatedKVStoreSync ;
432+
433+ impl < K : Deref > Deref for PaginatedKVStoreSyncWrapper < K >
434+ where
435+ K :: Target : PaginatedKVStoreSync ,
436+ {
437+ type Target = Self ;
438+ fn deref ( & self ) -> & Self :: Target {
439+ self
440+ }
441+ }
442+
443+ /// This is not exported to bindings users as async is only supported in Rust.
444+ impl < K : Deref > KVStore for PaginatedKVStoreSyncWrapper < K >
445+ where
446+ K :: Target : PaginatedKVStoreSync ,
447+ {
448+ fn read (
449+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str ,
450+ ) -> impl Future < Output = Result < Vec < u8 > , io:: Error > > + ' static + MaybeSend {
451+ let res = self . 0 . read ( primary_namespace, secondary_namespace, key) ;
452+
453+ async move { res }
454+ }
455+
456+ fn write (
457+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , buf : Vec < u8 > ,
458+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend {
459+ let res = self . 0 . write ( primary_namespace, secondary_namespace, key, buf) ;
460+
461+ async move { res }
462+ }
463+
464+ fn remove (
465+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , lazy : bool ,
466+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend {
467+ let res = self . 0 . remove ( primary_namespace, secondary_namespace, key, lazy) ;
468+
469+ async move { res }
470+ }
471+
472+ fn list (
473+ & self , primary_namespace : & str , secondary_namespace : & str ,
474+ ) -> impl Future < Output = Result < Vec < String > , io:: Error > > + ' static + MaybeSend {
475+ let res = self . 0 . list ( primary_namespace, secondary_namespace) ;
476+
477+ async move { res }
478+ }
479+ }
480+
481+ /// This is not exported to bindings users as async is only supported in Rust.
482+ impl < K : Deref > PaginatedKVStore for PaginatedKVStoreSyncWrapper < K >
483+ where
484+ K :: Target : PaginatedKVStoreSync ,
485+ {
486+ fn write_with_time (
487+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , creation_time : i64 ,
488+ buf : Vec < u8 > ,
489+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend {
490+ let res =
491+ self . 0 . write_with_time ( primary_namespace, secondary_namespace, key, creation_time, buf) ;
492+
493+ async move { res }
494+ }
495+
496+ fn list_paginated (
497+ & self , primary_namespace : & str , secondary_namespace : & str ,
498+ next_page_token : Option < ( String , i64 ) > ,
499+ ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + MaybeSend {
500+ let res = self . 0 . list_paginated ( primary_namespace, secondary_namespace, next_page_token) ;
501+
502+ async move { res }
503+ }
504+ }
505+
506+ /// Provides an interface that allows storage and retrieval of persisted values that are associated
507+ /// with given keys, with support for pagination with time-based ordering.
508+ ///
509+ /// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
510+ /// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
511+ /// ways, as long as per-namespace key uniqueness is asserted.
512+ ///
513+ /// Keys and namespaces are required to be valid ASCII strings in the range of
514+ /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
515+ /// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
516+ /// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
517+ /// should always be separated by primary namespace first, before secondary namespaces are used.
518+ /// While the number of primary namespaces will be relatively small and determined at compile time,
519+ /// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
520+ /// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
521+ /// and equally named primary or secondary namespaces must be avoided.
522+ ///
523+ /// **Note:** This trait extends the functionality of [`KVStore`] by adding support for
524+ /// paginated listing of keys based on a monotonic counter or logical timestamp. This is useful
525+ /// when dealing with a large number of keys that cannot be efficiently retrieved all at once.
526+ ///
527+ /// For a synchronous version of this trait, see [`PaginatedKVStoreSync`].
528+ ///
529+ /// This is not exported to bindings users as async is only supported in Rust.
530+ pub trait PaginatedKVStore : KVStore {
531+ /// Persists the given data under the given `key`.
532+ ///
533+ /// If the key does not exist, it will be created with the given `creation_time`. If the key
534+ /// already exists, the data will be updated but the original `creation_time` will be preserved.
535+ /// This ensures consistent pagination even when entries are updated.
536+ ///
537+ /// The `creation_time` parameter is an `i64` representing a monotonic counter or logical
538+ /// timestamp. It is used to track the order of keys for list operations, allowing results to
539+ /// be returned in descending order of creation time. Since `creation_time` is immutable after
540+ /// initial creation, pagination remains consistent even when entries are updated.
541+ ///
542+ /// Will create the given `primary_namespace` and `secondary_namespace` if not already present
543+ /// in the store.
544+ fn write_with_time (
545+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , creation_time : i64 ,
546+ buf : Vec < u8 > ,
547+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend ;
548+
549+ /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
550+ /// `primary_namespace`, ordered in descending order of `creation_time`.
551+ ///
552+ /// The `list_paginated` method returns the latest records first, based on the `creation_time`
553+ /// associated with each key. Pagination is controlled by the `next_page_token`, which is an
554+ /// `Option<(String, i64)>` used to determine the starting point for the next page of results.
555+ /// If `next_page_token` is `None`, the listing starts from the most recent entry. The
556+ /// `next_page_token` in the returned [`PaginatedListResponse`] can be used to fetch the next
557+ /// page of results.
558+ ///
559+ /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
560+ /// there are no more keys to return.
561+ fn list_paginated (
562+ & self , primary_namespace : & str , secondary_namespace : & str ,
563+ next_page_token : Option < ( String , i64 ) > ,
564+ ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + MaybeSend ;
565+ }
566+
350567/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`]
351568/// data migration.
352569pub trait MigratableKVStore : KVStoreSync {
@@ -1565,7 +1782,7 @@ mod tests {
15651782 use crate :: ln:: msgs:: BaseMessageHandler ;
15661783 use crate :: sync:: Arc ;
15671784 use crate :: util:: test_channel_signer:: TestChannelSigner ;
1568- use crate :: util:: test_utils:: { self , TestStore } ;
1785+ use crate :: util:: test_utils:: { self , TestPaginatedStore , TestStore } ;
15691786 use bitcoin:: hashes:: hex:: FromHex ;
15701787 use core:: cmp;
15711788
@@ -1975,4 +2192,78 @@ mod tests {
19752192 let store: Arc < dyn KVStoreSync + Send + Sync > = Arc :: new ( TestStore :: new ( false ) ) ;
19762193 assert ! ( persist_fn:: <_, TestChannelSigner >( Arc :: clone( & store) ) ) ;
19772194 }
2195+
2196+ #[ test]
2197+ fn paginated_store_basic_operations ( ) {
2198+ let store = TestPaginatedStore :: new ( 10 ) ;
2199+
2200+ // Write some data
2201+ store. write_with_time ( "ns1" , "ns2" , "key1" , 100 , vec ! [ 1 , 2 , 3 ] ) . unwrap ( ) ;
2202+ store. write_with_time ( "ns1" , "ns2" , "key2" , 200 , vec ! [ 4 , 5 , 6 ] ) . unwrap ( ) ;
2203+
2204+ // Read it back
2205+ assert_eq ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key1" ) . unwrap( ) , vec![ 1 , 2 , 3 ] ) ;
2206+ assert_eq ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key2" ) . unwrap( ) , vec![ 4 , 5 , 6 ] ) ;
2207+
2208+ // List should return keys in descending time order
2209+ let response = store. list_paginated ( "ns1" , "ns2" , None ) . unwrap ( ) ;
2210+ assert_eq ! ( response. keys, vec![ "key2" , "key1" ] ) ;
2211+ assert ! ( response. next_page_token. is_none( ) ) ;
2212+
2213+ // Remove a key
2214+ KVStoreSync :: remove ( & store, "ns1" , "ns2" , "key1" , false ) . unwrap ( ) ;
2215+ assert ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key1" ) . is_err( ) ) ;
2216+ }
2217+
2218+ #[ test]
2219+ fn paginated_store_pagination ( ) {
2220+ let store = TestPaginatedStore :: new ( 2 ) ;
2221+
2222+ // Write 5 items with different times
2223+ for i in 0 ..5i64 {
2224+ store. write_with_time ( "ns" , "" , & format ! ( "key{i}" ) , i, vec ! [ i as u8 ] ) . unwrap ( ) ;
2225+ }
2226+
2227+ // First page should have 2 items (highest times first: key4, key3)
2228+ let page1 = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2229+ assert_eq ! ( page1. keys. len( ) , 2 ) ;
2230+ assert_eq ! ( page1. keys, vec![ "key4" , "key3" ] ) ;
2231+ assert ! ( page1. next_page_token. is_some( ) ) ;
2232+
2233+ // Second page
2234+ let page2 = store. list_paginated ( "ns" , "" , page1. next_page_token ) . unwrap ( ) ;
2235+ assert_eq ! ( page2. keys. len( ) , 2 ) ;
2236+ assert_eq ! ( page2. keys, vec![ "key2" , "key1" ] ) ;
2237+ assert ! ( page2. next_page_token. is_some( ) ) ;
2238+
2239+ // Third page (last item)
2240+ let page3 = store. list_paginated ( "ns" , "" , page2. next_page_token ) . unwrap ( ) ;
2241+ assert_eq ! ( page3. keys. len( ) , 1 ) ;
2242+ assert_eq ! ( page3. keys, vec![ "key0" ] ) ;
2243+ assert ! ( page3. next_page_token. is_none( ) ) ;
2244+ }
2245+
2246+ #[ test]
2247+ fn paginated_store_update_preserves_creation_time ( ) {
2248+ let store = TestPaginatedStore :: new ( 10 ) ;
2249+
2250+ // Write items with specific creation times
2251+ store. write_with_time ( "ns" , "" , "key1" , 100 , vec ! [ 1 ] ) . unwrap ( ) ;
2252+ store. write_with_time ( "ns" , "" , "key2" , 200 , vec ! [ 2 ] ) . unwrap ( ) ;
2253+ store. write_with_time ( "ns" , "" , "key3" , 300 , vec ! [ 3 ] ) . unwrap ( ) ;
2254+
2255+ // Verify initial order (descending by creation_time)
2256+ let response = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2257+ assert_eq ! ( response. keys, vec![ "key3" , "key2" , "key1" ] ) ;
2258+
2259+ // Update key1 with a new creation_time that would put it first if used
2260+ store. write_with_time ( "ns" , "" , "key1" , 999 , vec ! [ 1 , 1 ] ) . unwrap ( ) ;
2261+
2262+ // Verify data was updated
2263+ assert_eq ! ( KVStoreSync :: read( & store, "ns" , "" , "key1" ) . unwrap( ) , vec![ 1 , 1 ] ) ;
2264+
2265+ // Verify order is unchanged - creation_time should have been preserved
2266+ let response = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2267+ assert_eq ! ( response. keys, vec![ "key3" , "key2" , "key1" ] ) ;
2268+ }
19782269}
0 commit comments