1use bytes::Bytes;
2use iroh_blobs::Hash;
3use iroh_blobs::api::Store as BlobStore;
4use iroh_docs::{
5 AuthorId, DocTicket, Entry, NamespaceId,
6 api::{
7 Doc,
8 protocol::{AddrInfoOptions, ShareMode},
9 },
10 engine::LiveEvent,
11 protocol::Docs,
12 store::Query,
13};
14use n0_future::StreamExt;
15
16use crate::{
17 constants::{CATALOG_METADATA_KEY, MODE_KEY, NETWORK_MEMBERS_KEY},
18 error::{Result, SyncwebError},
19};
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum ProvisionKind {
27 Folder,
28 Catalog,
29 NetworkMembership,
30}
31
32impl ProvisionKind {
33 const fn metadata_key(self) -> &'static [u8] {
34 match self {
35 Self::Folder => MODE_KEY,
36 Self::Catalog => CATALOG_METADATA_KEY,
37 Self::NetworkMembership => NETWORK_MEMBERS_KEY,
38 }
39 }
40}
41
42#[derive(Clone, Debug)]
44#[non_exhaustive]
45pub struct ProvisionedDoc {
46 pub doc: Doc,
47 pub ticket: DocTicket,
48 pub namespace_id: NamespaceId,
49}
50
51#[derive(Clone)]
52pub struct DocsEngine {
53 docs: Docs,
54 blob_store: BlobStore,
55 local_id: iroh::PublicKey,
58 relay_addr: Option<iroh_base::CustomAddr>,
61}
62
63impl DocsEngine {
64 #[must_use]
65 pub fn new(
66 docs: &Docs,
67 blob_store: &BlobStore,
68 local_id: iroh::PublicKey,
69 relay_addr: Option<iroh_base::CustomAddr>,
70 ) -> Self {
71 Self {
72 docs: docs.clone(),
73 blob_store: blob_store.clone(),
74 local_id,
75 relay_addr,
76 }
77 }
78
79 #[must_use]
80 pub const fn inner(&self) -> &Docs {
81 &self.docs
82 }
83
84 pub async fn create_namespace(&self) -> Result<Doc> {
88 self.docs
89 .create()
90 .await
91 .map_err(|error| SyncwebError::operation("failed to create namespace", error))
92 }
93
94 pub async fn create_or_open_namespace(&self, existing: Option<NamespaceId>) -> Result<(Doc, DocTicket)> {
106 let doc = match existing {
107 Some(namespace_id) => self
108 .open(namespace_id)
109 .await?
110 .ok_or(SyncwebError::NamespaceNotAvailable)?,
111 None => self.create_namespace().await?,
112 };
113 let ticket = self.share_ticket(&doc, true).await?;
114 Ok((doc, ticket))
115 }
116
117 pub async fn provision(
128 &self,
129 kind: ProvisionKind,
130 existing: Option<NamespaceId>,
131 metadata: impl AsRef<[u8]>,
132 ) -> Result<ProvisionedDoc> {
133 let (doc, ticket) = self.create_or_open_namespace(existing).await?;
134 let author = self.author().await?;
135 self.set(&doc, author, kind.metadata_key(), metadata).await?;
136 self.blob_store
137 .sync_db()
138 .await
139 .map_err(|error| SyncwebError::operation("failed to flush metadata blob", error))?;
140 let namespace_id = self.namespace_id(&doc);
141 Ok(ProvisionedDoc {
142 doc,
143 ticket,
144 namespace_id,
145 })
146 }
147
148 pub async fn import_ticket(&self, ticket: DocTicket) -> Result<Doc> {
152 self.docs
153 .import(ticket)
154 .await
155 .map_err(|error| SyncwebError::operation("failed to import document ticket", error))
156 }
157
158 pub async fn open(&self, namespace_id: NamespaceId) -> Result<Option<Doc>> {
162 self.docs
163 .open(namespace_id)
164 .await
165 .map_err(|error| SyncwebError::operation("failed to open namespace", error))
166 }
167
168 pub async fn drop_namespace(&self, namespace_id: NamespaceId) -> Result<()> {
172 self.docs
173 .drop_doc(namespace_id)
174 .await
175 .map_err(|error| SyncwebError::operation("failed to drop namespace", error))?;
176 Ok(())
177 }
178
179 pub async fn start_sync(&self, doc: &Doc, peers: Vec<iroh::EndpointAddr>) -> Result<()> {
188 doc.start_sync(peers)
189 .await
190 .map_err(|error| SyncwebError::operation("failed to start document synchronization", error))
191 }
192
193 pub async fn share(&self, doc: &Doc, mode: ShareMode) -> Result<DocTicket> {
197 self.share_with_options(doc, mode, AddrInfoOptions::RelayAndAddresses)
198 .await
199 }
200
201 pub async fn share_with_options(
209 &self,
210 doc: &Doc,
211 mode: ShareMode,
212 addr_options: AddrInfoOptions,
213 ) -> Result<DocTicket> {
214 let mut ticket = doc
215 .share(mode, addr_options)
216 .await
217 .map_err(|error| SyncwebError::operation("failed to share document", error))?;
218 if addr_options != AddrInfoOptions::Id
219 && let Some(relay_addr) = &self.relay_addr
220 {
221 for node in &mut ticket.nodes {
222 if node.id == self.local_id {
223 node.addrs.insert(iroh::TransportAddr::Custom(relay_addr.clone()));
224 }
225 }
226 }
227 Ok(ticket)
228 }
229
230 pub async fn share_ticket(&self, doc: &Doc, writable: bool) -> Result<DocTicket> {
236 self.share_ticket_with_options(doc, writable, AddrInfoOptions::RelayAndAddresses)
237 .await
238 }
239
240 pub async fn share_ticket_with_options(
247 &self,
248 doc: &Doc,
249 writable: bool,
250 addr_options: AddrInfoOptions,
251 ) -> Result<DocTicket> {
252 let mode = if writable { ShareMode::Write } else { ShareMode::Read };
253 self.share_with_options(doc, mode, addr_options).await
254 }
255
256 pub async fn author(&self) -> Result<AuthorId> {
260 self.docs
261 .author_default()
262 .await
263 .map_err(|error| SyncwebError::operation("failed to retrieve document author", error))
264 }
265
266 pub async fn export_author(&self, author_id: AuthorId) -> Result<Option<iroh_docs::Author>> {
270 self.docs
271 .author_export(author_id)
272 .await
273 .map_err(|error| SyncwebError::operation("failed to export document author", error))
274 }
275
276 pub async fn import_author(&self, author: iroh_docs::Author) -> Result<AuthorId> {
280 self.docs
281 .author_import(author.clone())
282 .await
283 .map_err(|error| SyncwebError::operation("failed to import document author", error))?;
284 Ok(author.id())
285 }
286
287 pub async fn set(
291 &self,
292 doc: &Doc,
293 author: AuthorId,
294 key: impl AsRef<[u8]>,
295 value: impl AsRef<[u8]>,
296 ) -> Result<Hash> {
297 doc.set_bytes(
298 author,
299 Bytes::copy_from_slice(key.as_ref()),
300 Bytes::copy_from_slice(value.as_ref()),
301 )
302 .await
303 .map_err(|error| SyncwebError::operation("failed to set document entry", error))
304 }
305
306 pub async fn set_blob(
310 &self,
311 doc: &Doc,
312 author: AuthorId,
313 key: impl AsRef<[u8]>,
314 hash: Hash,
315 size: u64,
316 ) -> Result<()> {
317 doc.set_hash(author, Bytes::copy_from_slice(key.as_ref()), hash, size)
318 .await
319 .map_err(|error| SyncwebError::operation("failed to set document blob", error))?;
320 Ok(())
321 }
322
323 pub async fn get(&self, doc: &Doc, author: AuthorId, key: impl AsRef<[u8]>) -> Result<Option<Entry>> {
327 doc.get_exact(author, key, false)
328 .await
329 .map_err(|error| SyncwebError::operation("failed to get document entry", error))
330 }
331
332 pub async fn get_any(&self, doc: &Doc, key: impl AsRef<[u8]>) -> Result<Option<Entry>> {
338 let entries = doc
339 .get_many(Query::single_latest_per_key().key_exact(key))
340 .await
341 .map_err(|error| SyncwebError::operation("failed to query document entries", error))?;
342 tokio::pin!(entries);
343 n0_future::StreamExt::next(&mut entries)
344 .await
345 .transpose()
346 .map_err(|error| SyncwebError::operation("failed to read document query result", error))
347 }
348
349 pub async fn list_latest(&self, doc: &Doc) -> Result<Vec<Entry>> {
355 let entries = doc
356 .get_many(Query::single_latest_per_key().build())
357 .await
358 .map_err(|error| SyncwebError::operation("failed to query document entries", error))?;
359 tokio::pin!(entries);
360 let mut output = Vec::new();
361 while let Some(entry_result) = n0_future::StreamExt::next(&mut entries).await {
362 let entry =
363 entry_result.map_err(|error| SyncwebError::operation("failed to read document entry", error))?;
364 output.push(entry);
365 }
366 Ok(output)
367 }
368
369 pub async fn list_all_entries(&self, doc: &Doc) -> Result<Vec<Entry>> {
380 let entries = doc
381 .get_many(Query::all().build())
382 .await
383 .map_err(|error| SyncwebError::operation("failed to query document entries", error))?;
384 tokio::pin!(entries);
385 let mut output = Vec::new();
386 while let Some(entry_result) = n0_future::StreamExt::next(&mut entries).await {
387 let entry =
388 entry_result.map_err(|error| SyncwebError::operation("failed to read document entry", error))?;
389 output.push(entry);
390 }
391 Ok(output)
392 }
393
394 pub async fn delete(&self, doc: &Doc, author: AuthorId, key: impl AsRef<[u8]>) -> Result<()> {
400 doc.del(author, key.as_ref().to_vec())
401 .await
402 .map_err(|error| SyncwebError::operation("failed to delete document entry", error))?;
403 Ok(())
404 }
405
406 pub async fn watch(
410 &self,
411 doc: &Doc,
412 ) -> Result<impl n0_future::Stream<Item = Result<LiveEvent>> + Send + Unpin + 'static> {
413 let stream = doc
414 .subscribe()
415 .await
416 .map_err(|error| SyncwebError::operation("failed to subscribe to document", error))?;
417 Ok(stream.map(|event| event.map_err(|error| SyncwebError::operation("document event failed", error))))
418 }
419
420 pub async fn watch_and_sync(
431 &self,
432 doc: &Doc,
433 ) -> Result<impl n0_future::Stream<Item = Result<LiveEvent>> + Send + Unpin + 'static> {
434 let live_events = self.watch(doc).await?;
435 self.start_sync(doc, Vec::new()).await?;
436 Ok(live_events)
437 }
438
439 #[must_use]
440 pub fn namespace_id(&self, doc: &Doc) -> NamespaceId {
441 doc.id()
442 }
443}