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