Skip to main content

p2panda_sync/
api.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#![allow(
4    rustdoc::redundant_explicit_links,
5    reason = "rust docs can't resolve links for SeqNum and LogId types due to a bug"
6)]
7
8//! Methods for building sync protocols using p2panda's [append-only log](p2panda_core::operation)
9//! for a wide range of network topologies and transports.
10//!
11//! This module contains all essential methods, types and helpers for querying local log state from
12//! the store, computing and comparing log state vectors, validating operation log integrity,
13//! buffering out-of-order operations, reading and writing operations and more.
14//!
15//! ## Examples
16//!
17//! See our [examples](https://github.com/p2panda/p2panda/tree/main/p2panda-sync/examples) folder
18//! for inspiration on how to achieve sync over a regular unicast connection or meshes with
19//! broadcast topologies, such as LoRa, BLE Advertisements, sneakernets or store-and-forward,
20//! delay-tolerant networks.
21//!
22//! ## Integration with `p2panda` processing layer
23//!
24//! p2panda strictly separates the _event delivery_ (`p2panda-net` and `p2panda-sync`) from the
25//! _event processing_ layer (`p2panda-stream`) while [`p2panda`](https://crates.io/crates/p2panda)
26//! is the out-of-the-box combination of both worlds.
27//!
28//! If you want to use your own sync protocol with `p2panda` you can currently establish an active
29//! topic handle and use the
30//! [`import`](https://docs.rs/p2panda/latest/p2panda/streams/struct.StreamPublisher.html#method.import)
31//! method to import your synced data into the topic stream with all features the high-level API
32//! brings.
33//!
34//! ## What is a sync protocol?
35//!
36//! Broadly speaking _sync_ or _replication_ protocols ensure that all involved nodes in a network
37//! will receive the same data, even if some of them have been unreachable for a while. Once nodes
38//! connect and _sync_, they'll exchange the data that each party has been missing.
39//!
40//! ```text
41//! Node A replica:
42//! [A] [B] [C]
43//!                                 Node B replica:
44//!                                     [A] [D] [E]
45//!
46//!              [Sync Protocol begins]
47//!
48//!                  <--- [D] [E]
49//!
50//!                  [B] [C] --->
51//!
52//!               [Sync Protocol ends]
53//!
54//! => Node A & B have the same state now: [A] [B] [C] [D] [E]
55//! ```
56//!
57//! The local state of a node is called a _replica_. It can be seen as a subset of "all data" in the
58//! network. With a sync protocol replicas want to converge to the same state. This underlying
59//! characteristic we call _eventual consistency_ which essentially means: All replicas will
60//! converge to the same state after "some" time, of course given that they have a chance to
61//! exchange data at some point. Eventual consistency is one of the core guarantees of any
62//! local-first system and therefore we always need a sync protocol.
63//!
64//! A naive, very spammy sync protocol would send everything all the time and eventually everyone
65//! would have received everything. To make things more efficient, replicas usually compute and
66//! exchange so called _state vectors_ first, informing others about their local replica state. The
67//! receiving nodes compute the difference between both replica's sets and send over only the
68//! missing data. Once all replicas have reached the same state, no data is exchanged anymore.
69//!
70//! **Different sync protocols, different characteristics**
71//!
72//! Sync protocols want to usually optimise towards different aspects: Is it inexpensive to compute
73//! the state and difference? Does it consume little memory? How many messages need to be sent
74//! before we can sync actual data? Can I sync only a subset of the data when I don't need
75//! everything?
76//!
77//! Another aspect is the security of the protocol: Can we make sure that the data sent to us is
78//! also what we asked for, is it guaranteed to be correct, or can an attacker exhaust all our
79//! computer resources before we've even noticed it?
80//!
81//! **Convergent data types**
82//!
83//! To achieve eventual consistency we do not only need a sync protocol but also a CRDT
84//! ([Conflict-free replicated data
85//! type](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)) which has the
86//! characteristic to make everyone deterministically _converge_ to the same state after all changes
87//! to the replicas have been observed by everyone. In p2panda we call this a _Convergent Data Type_
88//! (CDT) to isolate the term a bit more from CRDTs, which have become increasingly overloaded. CDTs
89//! are usually simple Last-Write-Wins Sets or LWW-Key-Value Ranges, Grow-Only-Sets or Append-Only
90//! Logs.
91//!
92//! **Why append-only logs?**
93//!
94//! In p2panda we've decided on append-only logs as the base convergent data type which delivers all
95//! application data. Application data can be shaped in any way and developers can express more
96//! advanced data types on top of logs via the [`Extensions`](p2panda_core::Extensions) or
97//! [`Body`](p2panda_core::Body), for example
98//! [DAGs](https://en.wikipedia.org/wiki/Directed_acyclic_graph) as we do for [causal
99//! ordering](https://docs.rs/p2panda-stream/latest/p2panda_stream/orderer/index.html) across
100//! multiple logs, [log-prefix
101//! pruning](https://docs.rs/p2panda-stream/latest/p2panda_stream/log_prune/index.html) or
102//! [Byzantine-Fault tolerance](https://arxiv.org/abs/2307.08381).
103//!
104//! We've chosen this to minimise messaging overhead (one message to exchange the state vector),
105//! enable efficient state comparison (constant time), and support sync protocols that do not assume
106//! a point-to-point (unicast) connection, especially where message throughput may be limited.
107//!
108//! Additionally, logs neatly represent the event sourcing aspects of peer-to-peer applications due
109//! to their natural representation of history and total ordering guarantees. In applications we
110//! usually observe a stream of _events_ to materialise application state.
111//!
112//! The research and experiments in this area are not exhausted and [interesting work is
113//! happening](https://github.com/ssbc/tinySSB) around using append-only logs.
114//!
115//! **Hourglass model**
116//!
117//! An entry in a log, the base convergent data type, can be also understood as the _narrow waist_,
118//! similar to the [Hourglass model](https://en.wikipedia.org/wiki/Hourglass_model) in computer
119//! networking. The sync protocol and log data type define the most atomic unit of delivery of data,
120//! only fulfilling that particular concern (eventual consistency) from which more complex layers
121//! can be expressed on top.
122//!
123//! ## Log-height sync
124//!
125//! p2panda uses append-only logs as the base convergent data type. It is the unit to be synced. We
126//! call an entry in the log an [`Operation`](p2panda_core::operation). Every operation in the log
127//! is labeled with a sequence number [`SeqNum`](p2panda_core::SeqNum), starting at zero. Building a
128//! sync protocol on top of logs is straight-forward: Nodes share their latest sequence number in
129//! the form of a state vector which is enough for any other node to compute the difference and send
130//! over the missing operations.
131//!
132//! ```text
133//! Node A replica:
134//! [0] <- [1] <- [2]
135//!                                 Node B replica:
136//!                                      [0] <- [1]
137//!
138//!              [Sync Protocol begins]
139//!
140//!                       (1) Node B computes state
141//!                        vector and announces it:
142//!
143//!          <--- "I have log height [1]"
144//!
145//! (2) Node A computes diff:
146//!
147//! [0] <- [1] <- [2]
148//!         ^
149//!    Remote state
150//!
151//! (3) Node A streams back missing operations for B:
152//!
153//!                    [2] --->
154//!
155//!                  (4) Node B validates operations
156//!                  and inserts them into database.
157//!
158//!               [Sync Protocol ends]
159//!
160//! => Node A & B have the same state now: [0] <- [1] <- [2]
161//! ```
162//!
163//! **Log heights as state vectors**
164//!
165//! The state vector of append-only logs is the [`LogHeights`] which indicate the HEAD (in git
166//! terms) or _frontier_ by stating the latest [`SeqNum`](p2panda_core::SeqNum). If more logs need
167//! to be exchanged the log heights are labelled with a unique
168//! [`Author`](p2panda_core::traits::Author) and [`LogId`](p2panda_core::LogId) tuple.
169//!
170//! **Topic -> log map**
171//!
172//! In p2panda we usually sync over [`Topic`](p2panda_core::Topic) which describes some sort of
173//! application data of interest to that node. Internally topics are collections of logs. We
174//! actively need to [associate](p2panda_store::topics::TopicStore::associate_tx) new logs to topics
175//! when creating or receiving them to keep the topic -> log mapping up-to-date.
176//!
177//! **Query and announce local log heights**
178//!
179//! > _Step (1) in figure above._
180//!
181//! To compute all associated log state vectors from a topic use the [`topic_log_heights`] method or
182//! [`log_heights`] to compute the state vectors from a list of log ids.
183//!
184//! [`LogHeights`] state vectors can be delivered in many ways, for example during a regular sync
185//! session over a direct bi-directional connection with another node or broadcast in a radio
186//! network. Whoever will receive this information can respond with the missing operations after
187//! computing the difference. This is how p2panda achieves sync functionality across various
188//! transports.
189//!
190//! **Computing difference between log heights**
191//!
192//! > _Step (2) in figure above._
193//!
194//! When receiving a remote state vector of another replica you want to use [`compare_logs`] to
195//! compute the difference with your local state. The resulting difference is [`LogRanges`] which
196//! can be used to query all missing operations for the other node from your database.
197//!
198//! **Query missing operations**
199//!
200//! > _Step (3) in figure above._
201//!
202//! You can use the [`log_ranges`] method to efficiently stream all operations from all logs given
203//! the computed difference. This is the data you finally want to send "over the wire" with your
204//! sync protocol.
205//!
206//! **Ingest received operations**
207//!
208//! > _Step (4) in figure above._
209//!
210//! Use [`ingest_operation`] to validate incoming operations and insert them into your local store.
211//! Use an out-of-order buffer [`OooBuffer`] if you can't guarantee that these operations will come
212//! in correct log order (monotonically incrementing sequence numbers) depending on your sync
213//! approach.
214//!
215//! ## Design considerations building a protocol
216//!
217//! When building your own sync protocol using p2panda's log data type, you might want to handle
218//! things differently than us. Here is a list of design considerations we've encountered when
219//! building different sync strategies which are helpful. This list is not exhaustive and we are
220//! sure there's many more creative ways to play with log-sync protocols!
221//!
222//! ### Validation
223//!
224//! Sync protocols might want to validate the operation format, log-integrity or even application
225//! data already during sync before it even reaches any higher layer. The [`ingest_operation`]
226//! method handles this part and can be enabled with the `ingest` feature flag. If no insertion into
227//! the database is necessary one can also use the
228//! [`validate_operation`](p2panda_core::operation::validate_operation) method.
229//!
230//! In p2panda's sync protocol implementations we chose to move validation into the event processing
231//! layer which happens outside of the sync- or _event delivery_ layer. Sync protocols only
232//! _forward_ incoming operations to any higher layer:
233//!
234//! ```text
235//! Event Delivery               Event Processing
236//! ==============               ================
237//!
238//! Sync + Transport     -->     Decryption -> Validation -> Insertion
239//!                                             |
240//!           <----------------------- Report back on error
241//!
242//!                      vs.
243//!
244//! Sync + Transport     -->     Insertion, etc.
245//!   + Validation
246//! ```
247//!
248//! This allows us to clearly separate these concerns for maximum flexibility. If data is fully
249//! encrypted, for example in support nodes or store-and-forward buffers, this separation becomes
250//! necessary, since the sync layer might not be aware of any encryption secrets or the process runs
251//! on a separate device. However, this separation implies that any higher processing layer needs to
252//! inform the sync protocol about potential violations or errors on-the-fly.
253//!
254//! Broker nodes might want to validate already during sync if the data is readable to them, so they
255//! can be sure to only stream validated data further to clients.
256//!
257//! ### Access Control
258//!
259//! Nobody should be able to sync data without our permission. How this is enforced depends on the
260//! underlying transport.
261//!
262//! Typical approaches include capability-based authorization, where presenting a token grants sync
263//! access to the requested data, or an access-control list managed by a CRDT or a user-managed
264//! database. These approaches generally assume that a confidential channel has been established
265//! between the parties. In store-and-forward contexts, we may instead need to rely more heavily on
266//! group encryption.
267//!
268//! ### Fast-push vs. sync
269//!
270//! Sync can be seen as a strategy of _catching-up_ on messages we've missed due to being offline or
271//! networking issues. In comparison to just sending out every new message as soon as it was created
272//! ("Fast-push") into the network sync is a more expensive operation. We can switch between these
273//! two modes in any order with different trade-offs:
274//!
275//! **Sync first, then fast-push**
276//!
277//! If the connection is cheap and fast (reliable internet with enough bandwidth) we can easily sync
278//! first and then eagerly push any new messages which have been created after the sync session has
279//! ended without starting to sync again. In p2panda we also call this _live mode_.
280//!
281//! Note that if any message was pushed eagerly to us _while_ we are syncing with that node, we need
282//! to buffer them (FIFO) and flush _after_ the sync session has ended to preserve ordering.
283//!
284//! **Fast-push first, then sync**
285//!
286//! In some network topologies or transports syncing might not be the first option due to bandwidth
287//! or message throughput restrictions. See NACK-implosion making things worse.
288//!
289//! In these cases we might want to optimistically always _flood_ the latest messages, even if nodes
290//! might be out-of-sync. If nodes detect that they are very far behind they might initiate a sync
291//! session _lazily_ ("slow repair"). Additionally nodes probably want to utilise some sort of
292//! [`OooBuffer`] to handle operations coming out-of-order.
293//!
294//! **NACK-implosions**
295//!
296//! In reliable multicast networking protocols we can understand
297//! [NACK](https://en.wikipedia.org/wiki/NACK-Oriented_Reliable_Multicast) as a request to
298//! retransmit packets which have been missed and _not_ acknowledged yet. This is very similar to a
299//! sync protocol.
300//!
301//! NACKing comes with its own issues: It can lead to spamming the network when multiple
302//! participants start announcing what they've missed and everyone starts streaming back the same
303//! missing operations until the network breaks down. This is what we call a _NACK-implosion_.
304//!
305//! For some protocols we want to mitigate these issue by _not_ always responding. One way of
306//! achieving this is to calculate a response probability in relation to the number of nearby
307//! network participants (ie. a density factor `d`); the greater the number of detected neighbours,
308//! the lower the chance of rebroadcasting a received message. This implies that sync can become
309//! slower.
310//!
311//! ### Out-of-order buffering
312//!
313//! Not every transport will guarantee an ordered connection (such as TCP) and data might arrive
314//! out-of-order. This can also happen if we eagerly publish operations without syncing first (see
315//! _Fast-push_).
316//!
317//! For this we want to buffer operations until the log-integrity is intact again. Usually we want
318//! to keep this buffer only in-memory with a fixed bound to prevent attacks which maliciously
319//! exhaust system resources with out-of-order data. In some scenarios with extreme delay-tolerance
320//! and out-of-order behavior we might want to persist the buffer. See [`OooBuffer`] for a
321//! ring-buffer implementation handling out-of-order operations.
322//!
323//! ### Partial replication
324//!
325//! p2panda supports partial replication in a rather simple way: Authors can publish application
326//! data in multiple logs and a sync protocol can choose to only replicate a subset of logs.
327//!
328//! ```text
329//! Author "panda":
330//! - "trees" log
331//! - "animals" log       -> Sync Protocol only requests all "trees" logs
332//!
333//! Author "icebear":
334//! - "animals" log
335//! ```
336//!
337//! Partial sync of a single log is only possible if the log does not contain any gaps due to the
338//! strict integrity guarantees of an append-only log. This means that in worst case the log needs
339//! to be replicated from the beginning until a desired point `n`.
340//!
341//! ```text
342//! [0] <- [1] <- [2]    -> Sync protocol only requests range [0..1]
343//! ```
344//!
345//! However, applications usually prune their logs and in practice we haven't observed long-growing
346//! logs which would require partial replication. In combination with selecting which logs to sync
347//! we believe these are enough facilities to express most applications' needs in terms of partial
348//! replication.
349//!
350//! If your sync protocol requires syncing only _the latest_ information, for example the latest `n`
351//! items of a log it will have to break the consistency guarantee of the log itself, which will
352//! require custom validation logic:
353//!
354//! ```text
355//! [0] <- .. <- [192] <- [193]    -> Sync protocol only requests range [99..102]
356//!
357//! Validation:
358//!
359//!     ... <- [99] <- [100] <- [101] <- [102]
360//!      ^
361//! Missing backlink
362//! ```
363//!
364//! In this case it would make more sense to design your logs in a way where one log represents for
365//! example one day of data being created. Instead of choosing to partially replicate _within_ the
366//! log you can now partially replicate the logs for the days you are interested in, for example the
367//! latest day:
368//!
369//! ```text
370//! - "trees 02.03.2026" log
371//! ...
372//! - "trees 09.03.2026" log  -> Sync protocol only requests "trees" logs >= 09.03.2026
373//! - "trees 10.03.2026" log
374//! ```
375//!
376//! If you still need more specific partial sync strategy, you might want to use your own base
377//! convergent data type or extend the existing ones, for example a merkle-tree or skip-list on top
378//! of a log which would be possible to express in the form of header
379//! [`Extensions`](p2panda_core::Extensions).
380//!
381//! ### Content Addressing
382//!
383//! p2panda usually uses [`Topic`](p2panda_core::Topic) to group append-only logs into a collection
384//! which is a form of content addressing. Applications _subscribe_ to topics to enable sync for
385//! this particular topic of interest. From now on they will receive logs grouped under this topic.
386//!
387//! The kind of addressing and mapping is generic in p2panda (usually expressed as a type `T`).
388//! Alternative approaches could for example offer a way to address data by its age, filtering out
389//! logs which contain only operations older than a specified date.
390//!
391//! ### Topic association and selection
392//!
393//! A single author's log is usually associated with one or many topics. Authors can also have
394//! multiple logs associated with a topic:
395//!
396//! ```text
397//! Author   I / Log 1 ┐
398//! Author  II / Log 2 │ <- Topic A
399//! Author III / Log 1 │
400//! Author III / Log 2 ┘
401//!
402//! Author   I / Log 1 ┐
403//! Author  II / Log 2 │ <- Topic B
404//! Author   I / Log 3 ┘
405//! ```
406//!
407//! **When and how do we associate**
408//!
409//! Association usually takes place when _creating_ or _ingesting_ operations and is normally _not_
410//! encoded in the operation itself. A sync protocol however needs to deliver the topic (either
411//! through a topic handshake before or metadata around the operation) to be able to correctly
412//! associate the log with the topic when receiving it from a remote node.
413//!
414//! **Confidential topic handshake**
415//!
416//! In p2panda we disassociate the topic from any immutable data, such as operations, to reduce
417//! potential metadata leakage, and on top of that, we aim to encrypt messages fully, either via
418//! transport encryption (TLS 1.3 over QUIC) or using `p2panda-spaces` and only exchange topics
419//! [confidentially](https://docs.rs/p2panda-discovery/latest/p2panda_discovery/). See _Metadata &
420//! privacy_ for more.
421//!
422//! **Active topics**
423//!
424//! A sync session does not need to run over _all_ topics the node has ever used. Often we only want
425//! to sync over the current active _topic handles_, especially when resources are limited we don't
426//! want to overwhelm the network with talking about everything which might already be outdated and
427//! redundant.
428//!
429//! ```text
430//! All known topics ever used by a node:
431//!
432//!  A
433//!  B
434//!  C ┐
435//!  D │ <- Active topics to sync (subset)
436//!  E ┘
437//!  F
438//! ...
439//! ```
440//!
441//! ### Pruning
442//!
443//! Logs can be [pruned](p2panda_core::prune) in p2panda by announcing a pruning point after which
444//! any node can remove the prefix of the log.
445//!
446//! ```text
447//! <- [8] <- [9] <- [10]
448//!     ^
449//!  Pruning point
450//! ```
451//!
452//! Usually pruning points are encoded in header extensions and only dealt with on _event
453//! processing_ layer, outside the sync protocol. However, some sync protocols might want to include
454//! the _pruning frontier_ as part of the state vector to indicate that the other nodes do not need
455//! to send outdated operations, even if they still have them from their perspective (because they
456//! didn't observe the pruning event yet).
457//!
458//! ### Metadata & privacy
459//!
460//! Some connectivity substrates come with less than ideal or almost no privacy, for example sending
461//! messages via mDNS or packet radio. Data might not be natively protected by transport encryption
462//! or other measures. For multicast or broadcast networks we recommend encrypting the payloads
463//! before publishing them.
464//!
465//! **Multicast transport-encryption**
466//!
467//! One approach can be to use the `Topic` as a symmetric secret (AEAD), if revocation is required
468//! one can utilise [p2panda's key agreement
469//! scheme](https://docs.rs/p2panda-spaces/latest/p2panda_spaces/) and export the latest symmetric
470//! secret key from there. Encrypting payloads as such will result in some sort of _Multicast
471//! "Transport Encryption"_ (encrypting messages for transport towards a group instead of another
472//! single entity).
473//!
474//! **Metadata leakage**
475//!
476//! Sync protocol implementers should be careful with choosing what information they want to include
477//! in publicly readable coordination- or sync messages, such as public keys, user identifiers or
478//! topics. Any observer of the network would be able to correlate devices and their interest in
479//! certain topics.
480//!
481//! **Confidential sync**
482//!
483//! In p2panda we attempt to minimize metadata leakage to almost zero by full encryption of payloads
484//! and only establish sync confidentially. In an authenticated transport channel (TLS 1.3 etc.) we
485//! can rely on [private equality test](https://docs.rs/p2panda-discovery/latest/p2panda_discovery/)
486//! before exchanging data, while in a more multicast setting we can use a _proof of decryption_ (we
487//! could successfully decrypt a constant value using a secret key which associates us with the same
488//! topic) to learn that the other party has the same interest as us.
489//!
490//! ### Mitigating spam
491//!
492//! To isolate spammers from a network we either want to avoid connecting to them or prevent
493//! spreading their data in the network if we don't have a notion of _connections_ (radio
494//! broadcast). In both cases this can be solved with some sort of _deny list_.
495//!
496//! This means that we need a form of authenticity guarantee (digital signature) for each message
497//! delivered over the network so we can reliably verify the authorship and origin of them.
498//!
499//! Mitigating spam comes as a trade-off of metadata leakage as we have to introduce some sort of
500//! device id (or similar) and signing scheme. Sync protocol designers should try to separate these
501//! identifiers from the ones used to identify operation authors.
502//!
503//! ### Store-and-forward
504//!
505//! Some network topologies require data to be transmitted through intermediaries if we can't or
506//! don't want to establish direct connections. This is especially the case for delay-tolerant mesh
507//! networks.
508//!
509//! ```text
510//! Node A wants to send a message m to Node C but can't establish a direct connection. They choose
511//! to sent the message via B.
512//!
513//! [A] -- m --> [B] -- m --> [C]
514//!               ^
515//!  B needs to store-and-forward m
516//! ```
517//!
518//! In these kinds of networks, nodes need to sometimes sync data they are _not interested_ in,
519//! meaning that they don't know the topic of this data or do not actively process it (we sometimes
520//! say that there's no "active topic handle").
521//!
522//! This can be achieved with a _store-and-forward_ approach, while it is up to the implementer to
523//! decide how much and how long (ring-buffer, expiry date, ..) and for whom they want to store (per
524//! topic, device, access via capability, ..) unknown data.
525//!
526//! Store-and-forward buffers do not always need to reside next to the node but can also be
527//! external, such as USB-sticks or p2panda support nodes which behave more like external _mailbox_
528//! servers.
529//!
530//! Ideally all data is fully encrypted and its shape and contents oblivious to anyone. Usually we
531//! can't leverage any "data type tricks", such as log-height sync, in these cases or we need to
532//! build hybrids where some information about the base data type (backlink, etc.) is still
533//! available in plaintext and readable by everyone.
534//!
535//! As an example we've implemented a data-type-agnostic, generic store-and-forward solution named
536//! [MemoryLAN](https://github.com/p2panda/memorylan) based on a bounded, probabilistic
537//! Cuckoo-Filter sync protocol for unknown data. This can be integrated as a base layer for any
538//! sync protocol on top of a mesh.
539//!
540//! ### Bandwidth & message throughput
541//!
542//! Some transports have limitations in the form of maximum packet size. For example, a radio-based
543//! transport may restrict us to packets smaller than 255 bytes and may take several seconds to
544//! transmit.
545//!
546//! In order to deal with these limitations, some higher-level protocols may be required. This could
547//! take the form of a negotiation protocol allowing channel capacity to be allocated on a per-topic
548//! basis. Another protocol might ensure that sync happens lazily (on demand) to ensure a minimum of
549//! unnecessary messages being broadcast. Finally, one could compact the payload size of state
550//! vectors with the help of probabilistic filters (ie. bloom-, cuckoo-filters etc.).
551//!
552//! Transports might require some sort of message framing in case the limits have been reached with
553//! one single message, however this is out of scope for a sync protocol and rather a transport
554//! concern.
555//!
556//! [`ingest_operation`]: <https://docs.rs/p2panda-stream/latest/p2panda_stream/ingest/fn.ingest_operation.html>
557//! [`OooBuffer`]: <https://docs.rs/p2panda-stream/latest/p2panda_stream/ingest/struct.OooBuffer.html>
558use futures_util::stream;
559use futures_util::{StreamExt, future};
560pub use p2panda_core::logs::{LogHeights, LogRanges, Logs, compare_logs};
561use p2panda_core::{AnyOperation, Hash, LogId, SeqNum, VerifyingKey};
562use p2panda_store::logs::LogStore;
563use p2panda_store::topics::TopicStore;
564#[cfg(feature = "ingest")]
565pub use p2panda_stream::ingest::{
566    IngestError, IngestResult, OooBuffer, OooResult, ingest_operation, validate_operation,
567};
568use thiserror::Error;
569
570/// Item delivered from a [`LogStream`].
571pub type LogEntry<L> = p2panda_store::logs::LogEntry<AnyOperation, L>;
572
573/// Stream of operations in a log range.
574pub type LogStream<L, E> = p2panda_store::logs::LogStream<AnyOperation, L, E>;
575
576/// Compute log heights of all passed author logs based on what is known in the local store.
577pub async fn log_heights<L, S>(
578    store: &S,
579    logs: &Logs<VerifyingKey, L>,
580) -> Result<LogHeights<VerifyingKey, L>, S::Error>
581where
582    L: LogId,
583    S: LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>,
584{
585    let mut result = LogHeights::new();
586    for (verifying_key, log_ids) in logs {
587        let Some(log_heights) = store.get_log_heights(verifying_key, log_ids).await? else {
588            continue;
589        };
590        result.insert(*verifying_key, log_heights);
591    }
592
593    Ok(result)
594}
595
596/// Construct a stream which returns all operations in the provided log ranges.
597///
598/// This is a memory efficient query with only one stream item staying in memory at a time. If any
599/// error occurs when fetching items the stream will return the error then immediately close.
600pub fn log_ranges<L, S>(store: &S, ranges: LogRanges<VerifyingKey, L>) -> LogStream<L, S::Error>
601where
602    L: LogId + Send + 'static,
603    S: LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash> + Clone + Send + 'static,
604{
605    let mut flattened_ranges = vec![];
606    for (author, log_heights) in ranges {
607        for (log_id, (after, until)) in log_heights {
608            flattened_ranges.push((author, log_id, after, until));
609        }
610    }
611
612    let store = store.clone();
613
614    let stream = stream::iter(flattened_ranges)
615        // Flatten all inner streams into one.
616        .flat_map(move |(author, log_id, after, until)| {
617            match store.log_entries(&author, &log_id, after, until) {
618                Ok(stream) => stream,
619                // If an error occurs when constructing the next stream, return a new stream
620                // containing one item, the error itself.
621                Err(err) => Box::pin(stream::once(async move { Err(err) })),
622            }
623        })
624        // If any error is observed then immediately close the stream.
625        .scan(false, |error_occurred, item| {
626            if *error_occurred {
627                return future::ready(None);
628            }
629            *error_occurred = item.is_err();
630            future::ready(Some(item))
631        });
632
633    Box::pin(stream)
634}
635
636/// Compute log heights for the given topic based on what is known in the local store.
637pub async fn topic_log_heights<L, S, T>(
638    store: &S,
639    topic: &T,
640) -> Result<LogHeights<VerifyingKey, L>, StoreError<L, S, T>>
641where
642    L: LogId,
643    S: TopicStore<T, VerifyingKey, L> + LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>,
644{
645    let logs = store
646        .resolve(topic)
647        .await
648        .map_err(|err| StoreError::TopicStore(err))?;
649    let log_heights = log_heights(store, &logs)
650        .await
651        .map_err(|err| StoreError::LogStore(err))?;
652
653    Ok(log_heights)
654}
655
656/// Critical store failure.
657#[derive(Debug, Error)]
658pub enum StoreError<L, S, T>
659where
660    L: LogId,
661    S: TopicStore<T, VerifyingKey, L> + LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>,
662{
663    #[error(transparent)]
664    LogStore(<S as LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>>::Error),
665
666    #[error(transparent)]
667    TopicStore(<S as TopicStore<T, VerifyingKey, L>>::Error),
668}