@@ -347,6 +347,205 @@ pub trait KVStore {
347347 ) -> impl Future < Output = Result < Vec < String > , io:: Error > > + ' static + MaybeSend ;
348348}
349349
350+ /// An opaque token used for paginated listing operations.
351+ ///
352+ /// This token should be treated as an opaque value by callers. Pass the token returned from
353+ /// one `list_paginated` call to the next call to continue pagination. The internal format
354+ /// is implementation-defined and may change between versions.
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 a token for retrieving the next page of results.
361+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
362+ pub struct PaginatedListResponse {
363+ /// A vector of keys, ordered from most recently created to least recently created.
364+ pub keys : Vec < String > ,
365+
366+ /// A token that can be passed to the next call to continue pagination.
367+ ///
368+ /// Is `None` if there are no more pages to retrieve.
369+ pub next_page_token : Option < PageToken > ,
370+ }
371+
372+ /// Provides an interface that allows storage and retrieval of persisted values that are associated
373+ /// with given keys, with support for pagination.
374+ ///
375+ /// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
376+ /// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
377+ /// ways, as long as per-namespace key uniqueness is asserted.
378+ ///
379+ /// Keys and namespaces are required to be valid ASCII strings in the range of
380+ /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
381+ /// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
382+ /// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
383+ /// should always be separated by primary namespace first, before secondary namespaces are used.
384+ /// While the number of primary namespaces will be relatively small and determined at compile time,
385+ /// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
386+ /// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
387+ /// and equally named primary or secondary namespaces must be avoided.
388+ ///
389+ /// **Note:** This trait extends the functionality of [`KVStoreSync`] by adding support for
390+ /// paginated listing of keys in creation order. This is useful when dealing with a large number
391+ /// of keys that cannot be efficiently retrieved all at once.
392+ ///
393+ /// For an asynchronous version of this trait, see [`PaginatedKVStore`].
394+ pub trait PaginatedKVStoreSync : KVStoreSync {
395+ /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
396+ /// `primary_namespace`, ordered from most recently created to least recently created.
397+ ///
398+ /// Implementations must return keys in reverse creation order (newest first). How creation
399+ /// order is tracked is implementation-defined (e.g., storing creation timestamps, using an
400+ /// incrementing ID, or another mechanism). Creation order (not last-updated order) is used
401+ /// to prevent race conditions during pagination: if keys were ordered by update time, a key
402+ /// updated mid-pagination could shift position, causing it to be skipped or returned twice
403+ /// across pages.
404+ ///
405+ /// If `page_token` is provided, listing continues from where the previous page left off.
406+ /// If `None`, listing starts from the most recently created entry. The `next_page_token`
407+ /// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch
408+ /// the next page.
409+ ///
410+ /// Implementations must generate a [`PageToken`] that encodes enough information to resume
411+ /// listing from the correct position. The token should encode the creation timestamp (or
412+ /// sequence number) and key name of the last returned entry. Tokens must remain valid across
413+ /// multiple calls within a reasonable timeframe. If the entry referenced by a token has been
414+ /// deleted, implementations should resume from the next valid position rather than failing.
415+ /// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should
416+ /// not be used across different namespace pairs.
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 , page_token : Option < PageToken > ,
422+ ) -> Result < PaginatedListResponse , io:: Error > ;
423+ }
424+
425+ /// A wrapper around a [`PaginatedKVStoreSync`] that implements the [`PaginatedKVStore`] trait.
426+ /// It is not necessary to use this type directly.
427+ #[ derive( Clone ) ]
428+ pub struct PaginatedKVStoreSyncWrapper < K : Deref > ( pub K )
429+ where
430+ K :: Target : PaginatedKVStoreSync ;
431+
432+ impl < K : Deref > Deref for PaginatedKVStoreSyncWrapper < K >
433+ where
434+ K :: Target : PaginatedKVStoreSync ,
435+ {
436+ type Target = Self ;
437+ fn deref ( & self ) -> & Self :: Target {
438+ self
439+ }
440+ }
441+
442+ /// This is not exported to bindings users as async is only supported in Rust.
443+ impl < K : Deref > KVStore for PaginatedKVStoreSyncWrapper < K >
444+ where
445+ K :: Target : PaginatedKVStoreSync ,
446+ {
447+ fn read (
448+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str ,
449+ ) -> impl Future < Output = Result < Vec < u8 > , io:: Error > > + ' static + MaybeSend {
450+ let res = self . 0 . read ( primary_namespace, secondary_namespace, key) ;
451+
452+ async move { res }
453+ }
454+
455+ fn write (
456+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , buf : Vec < u8 > ,
457+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend {
458+ let res = self . 0 . write ( primary_namespace, secondary_namespace, key, buf) ;
459+
460+ async move { res }
461+ }
462+
463+ fn remove (
464+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , lazy : bool ,
465+ ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + MaybeSend {
466+ let res = self . 0 . remove ( primary_namespace, secondary_namespace, key, lazy) ;
467+
468+ async move { res }
469+ }
470+
471+ fn list (
472+ & self , primary_namespace : & str , secondary_namespace : & str ,
473+ ) -> impl Future < Output = Result < Vec < String > , io:: Error > > + ' static + MaybeSend {
474+ let res = self . 0 . list ( primary_namespace, secondary_namespace) ;
475+
476+ async move { res }
477+ }
478+ }
479+
480+ /// This is not exported to bindings users as async is only supported in Rust.
481+ impl < K : Deref > PaginatedKVStore for PaginatedKVStoreSyncWrapper < K >
482+ where
483+ K :: Target : PaginatedKVStoreSync ,
484+ {
485+ fn list_paginated (
486+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
487+ ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + MaybeSend {
488+ let res = self . 0 . list_paginated ( primary_namespace, secondary_namespace, page_token) ;
489+
490+ async move { res }
491+ }
492+ }
493+
494+ /// Provides an interface that allows storage and retrieval of persisted values that are associated
495+ /// with given keys, with support for pagination.
496+ ///
497+ /// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
498+ /// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
499+ /// ways, as long as per-namespace key uniqueness is asserted.
500+ ///
501+ /// Keys and namespaces are required to be valid ASCII strings in the range of
502+ /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
503+ /// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
504+ /// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
505+ /// should always be separated by primary namespace first, before secondary namespaces are used.
506+ /// While the number of primary namespaces will be relatively small and determined at compile time,
507+ /// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
508+ /// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
509+ /// and equally named primary or secondary namespaces must be avoided.
510+ ///
511+ /// **Note:** This trait extends the functionality of [`KVStore`] by adding support for
512+ /// paginated listing of keys in creation order. This is useful when dealing with a large number
513+ /// of keys that cannot be efficiently retrieved all at once.
514+ ///
515+ /// For a synchronous version of this trait, see [`PaginatedKVStoreSync`].
516+ ///
517+ /// This is not exported to bindings users as async is only supported in Rust.
518+ pub trait PaginatedKVStore : KVStore {
519+ /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
520+ /// `primary_namespace`, ordered from most recently created to least recently created.
521+ ///
522+ /// Implementations must return keys in reverse creation order (newest first). How creation
523+ /// order is tracked is implementation-defined (e.g., storing creation timestamps, using an
524+ /// incrementing ID, or another mechanism). Creation order (not last-updated order) is used
525+ /// to prevent race conditions during pagination: if keys were ordered by update time, a key
526+ /// updated mid-pagination could shift position, causing it to be skipped or returned twice
527+ /// across pages.
528+ ///
529+ /// If `page_token` is provided, listing continues from where the previous page left off.
530+ /// If `None`, listing starts from the most recently created entry. The `next_page_token`
531+ /// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch
532+ /// the next page.
533+ ///
534+ /// Implementations must generate a [`PageToken`] that encodes enough information to resume
535+ /// listing from the correct position. The token should encode the creation timestamp (or
536+ /// sequence number) and key name of the last returned entry. Tokens must remain valid across
537+ /// multiple calls within a reasonable timeframe. If the entry referenced by a token has been
538+ /// deleted, implementations should resume from the next valid position rather than failing.
539+ /// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should
540+ /// not be used across different namespace pairs.
541+ ///
542+ /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
543+ /// there are no more keys to return.
544+ fn list_paginated (
545+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
546+ ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + MaybeSend ;
547+ }
548+
350549/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`]
351550/// data migration.
352551pub trait MigratableKVStore : KVStoreSync {
@@ -1565,7 +1764,7 @@ mod tests {
15651764 use crate :: ln:: msgs:: BaseMessageHandler ;
15661765 use crate :: sync:: Arc ;
15671766 use crate :: util:: test_channel_signer:: TestChannelSigner ;
1568- use crate :: util:: test_utils:: { self , TestStore } ;
1767+ use crate :: util:: test_utils:: { self , TestPaginatedStore , TestStore } ;
15691768 use bitcoin:: hashes:: hex:: FromHex ;
15701769 use core:: cmp;
15711770
@@ -1975,4 +2174,78 @@ mod tests {
19752174 let store: Arc < dyn KVStoreSync + Send + Sync > = Arc :: new ( TestStore :: new ( false ) ) ;
19762175 assert ! ( persist_fn:: <_, TestChannelSigner >( Arc :: clone( & store) ) ) ;
19772176 }
2177+
2178+ #[ test]
2179+ fn paginated_store_basic_operations ( ) {
2180+ let store = TestPaginatedStore :: new ( 10 ) ;
2181+
2182+ // Write some data
2183+ store. write ( "ns1" , "ns2" , "key1" , vec ! [ 1 , 2 , 3 ] ) . unwrap ( ) ;
2184+ store. write ( "ns1" , "ns2" , "key2" , vec ! [ 4 , 5 , 6 ] ) . unwrap ( ) ;
2185+
2186+ // Read it back
2187+ assert_eq ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key1" ) . unwrap( ) , vec![ 1 , 2 , 3 ] ) ;
2188+ assert_eq ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key2" ) . unwrap( ) , vec![ 4 , 5 , 6 ] ) ;
2189+
2190+ // List should return keys in descending order
2191+ let response = store. list_paginated ( "ns1" , "ns2" , None ) . unwrap ( ) ;
2192+ assert_eq ! ( response. keys, vec![ "key2" , "key1" ] ) ;
2193+ assert ! ( response. next_page_token. is_none( ) ) ;
2194+
2195+ // Remove a key
2196+ KVStoreSync :: remove ( & store, "ns1" , "ns2" , "key1" , false ) . unwrap ( ) ;
2197+ assert ! ( KVStoreSync :: read( & store, "ns1" , "ns2" , "key1" ) . is_err( ) ) ;
2198+ }
2199+
2200+ #[ test]
2201+ fn paginated_store_pagination ( ) {
2202+ let store = TestPaginatedStore :: new ( 2 ) ;
2203+
2204+ // Write 5 items with different order values
2205+ for i in 0 ..5i64 {
2206+ store. write ( "ns" , "" , & format ! ( "key{i}" ) , vec ! [ i as u8 ] ) . unwrap ( ) ;
2207+ }
2208+
2209+ // First page should have 2 items (most recently created first: key4, key3)
2210+ let page1 = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2211+ assert_eq ! ( page1. keys. len( ) , 2 ) ;
2212+ assert_eq ! ( page1. keys, vec![ "key4" , "key3" ] ) ;
2213+ assert ! ( page1. next_page_token. is_some( ) ) ;
2214+
2215+ // Second page
2216+ let page2 = store. list_paginated ( "ns" , "" , page1. next_page_token ) . unwrap ( ) ;
2217+ assert_eq ! ( page2. keys. len( ) , 2 ) ;
2218+ assert_eq ! ( page2. keys, vec![ "key2" , "key1" ] ) ;
2219+ assert ! ( page2. next_page_token. is_some( ) ) ;
2220+
2221+ // Third page (last item)
2222+ let page3 = store. list_paginated ( "ns" , "" , page2. next_page_token ) . unwrap ( ) ;
2223+ assert_eq ! ( page3. keys. len( ) , 1 ) ;
2224+ assert_eq ! ( page3. keys, vec![ "key0" ] ) ;
2225+ assert ! ( page3. next_page_token. is_none( ) ) ;
2226+ }
2227+
2228+ #[ test]
2229+ fn paginated_store_update_preserves_order ( ) {
2230+ let store = TestPaginatedStore :: new ( 10 ) ;
2231+
2232+ // Write items with specific order values
2233+ store. write ( "ns" , "" , "key1" , vec ! [ 1 ] ) . unwrap ( ) ;
2234+ store. write ( "ns" , "" , "key2" , vec ! [ 2 ] ) . unwrap ( ) ;
2235+ store. write ( "ns" , "" , "key3" , vec ! [ 3 ] ) . unwrap ( ) ;
2236+
2237+ // Verify initial order (newest first)
2238+ let response = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2239+ assert_eq ! ( response. keys, vec![ "key3" , "key2" , "key1" ] ) ;
2240+
2241+ // Update key1 with a new order value that would put it first if used
2242+ store. write ( "ns" , "" , "key1" , vec ! [ 1 , 1 ] ) . unwrap ( ) ;
2243+
2244+ // Verify data was updated
2245+ assert_eq ! ( KVStoreSync :: read( & store, "ns" , "" , "key1" ) . unwrap( ) , vec![ 1 , 1 ] ) ;
2246+
2247+ // Verify order is unchanged - creation order should have been preserved
2248+ let response = store. list_paginated ( "ns" , "" , None ) . unwrap ( ) ;
2249+ assert_eq ! ( response. keys, vec![ "key3" , "key2" , "key1" ] ) ;
2250+ }
19782251}
0 commit comments