Skip to content

Commit 12efb30

Browse files
authored
feat(hermes): Implement IPFS publishing workflow (#694)
* feat: implement doc-sync channel::post API for issue #628 - Add channel::post(document_bytes) API as requested - Integrate hermes-ipfs library for IPFS operations - Add HTTP endpoints for testing (/api/doc-sync/*) - Configure HTTP gateway routing - Implement IPFS add, pin, and PubSub publish workflow * feat: implement doc-sync channel::post API for issue #628 - Add channel::post(document_bytes) API as requested - Integrate hermes-ipfs library for IPFS operations - Add HTTP endpoints for testing (/api/doc-sync/*) - Configure HTTP gateway routing - Implement IPFS add, pin, and PubSub publish workflow * hermes ipfs version * fix: enable WASM compilation for doc-sync module and add host stubs WASM compilation fixes: - Made tokio runtime features conditional (rt-multi-thread only for non-WASM) - Added separate WASM/native implementations using futures::executor for WASM - Conditionally compile Runtime usage and block_on calls Host implementation: - Replaced panicking todo!() with warning messages and stub return values - Added Resource stub creation for SyncChannel::new() - Functions now print warnings but don't crash runtime This allows the doc-sync module to compile for wasm32-wasip2 targets and run without panicking, though full functionality requires proper host implementation. * Remove all cfg attributes and consolidate WASM/non-WASM code paths into single implementations using futures::executor. Simplify HTTP handlers and reduce complexity to clearly demonstrate the 4-step workflow. * refactor(doc-sync): simplify for demo workflow Remove conditional compilation, OnceLock pattern, and unnecessary complexity to clearly show the 4-step IPFS PubSub workflow. * refactor(doc-sync): use WIT bindings directly for demo Replace async hermes-ipfs library with direct WIT function calls (file_add, file_pin, pubsub_publish). Remove conditional compilation and async dependencies to simplify the 4-step workflow demo. * refactor(doc-sync): use WIT bindings directly for demo Replace async hermes-ipfs library with direct WIT function calls (file_add, file_pin, pubsub_publish). Remove conditional compilation and async dependencies to simplify the 4-step workflow demo. * refactor(doc-sync): integrate with PR #691 subscription flow Replace async hermes-ipfs library with synchronous WIT bindings (file_add, file_pin, pubsub_publish, pubsub_subscribe). Add actual channel subscription in SyncChannel::new() and document complete pub/sub flow with PR #691 infrastructure. Changes: - Use WIT IPFS functions directly instead of async library - Call pubsub_subscribe() to register DocSync subscriptions - Document how on_new_doc events are triggered by PR #691 - Remove conditional compilation and async dependencies - Show clear 4-step workflow: add → pin → validate → publish * docs(doc-sync): document PR #691 integration requirements Add comprehensive comments explaining PR #691 requirement for subscription event routing. Document the complete pub/sub flow, what works now vs what needs PR #691, and how to integrate when it merges * fix(doc-sync): import GuestSyncChannel trait and clarify PR #691 comment Fix compilation error and clarify that publishing to PubSub works now; PR #691 is only needed to route incoming messages to event handlers. * update docs * refactor(doc-sync): Focus module on publishing workflow only Remove subscription logic and simplify documentation. Module now demonstrates only the 4-step publishing workflow: file_add, file_pin, pre-publish validation, and pubsub_publish. * fmt * refactor(doc-sync): Move post logic to host Execute the 4-step publishing workflow (file_add, file_pin, pre-publish, pubsub_publish) on the host side instead of in the WASM module. Reduces boundary crossings from 6 to 2 for better performance. * fmt * fmt * fix(doc-sync): Fix compilation errors - Fix SyncChannel resource import and usage - Update channel::post() to call host implementation correctly * refactor(doc-sync): Replace eprintln with tracing Use tracing macros (info/warn/error) instead of eprintln for logging in doc-sync host implementation. * refactor(doc-sync): Replace eprintln with tracing Use tracing macros (info/warn/error) instead of eprintln for logging in doc-sync host implementation. * fmt * refactor(doc-sync): Extract constants and improve error logging - Add DOC_SYNC_TOPIC and DOC_SYNC_CHANNEL constants - Add error logging to id_for() method * refactor(doc-sync): Improve error logging and remove redundant conversions - Log actual error details instead of discarding with - Remove redundant .to_string() on CID response - Add error logging for failed post operations * fmt
1 parent dee73bc commit 12efb30

File tree

12 files changed

+308
-59
lines changed

12 files changed

+308
-59
lines changed

hermes/Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hermes/apps/athena/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hermes/apps/athena/manifest_app.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
{
2828
"package": "modules/auth/lib/auth.hmod",
2929
"name": "auth"
30+
},
31+
{
32+
"package": "modules/doc-sync/lib/doc_sync.hmod",
33+
"name": "doc_sync"
3034
}
3135
],
3236
"www": "modules/http-proxy/lib/www"
33-
}
37+
}

hermes/apps/athena/modules/doc-sync/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ crate-type = ["cdylib"]
1212

1313
[dependencies]
1414
shared = { version = "0.1.0", path = "../../shared" }
15+
serde_json = "1.0.145"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Doc Sync Module
2+
3+
Thin wrapper for posting documents to IPFS PubSub.
4+
The actual 4-step workflow (file_add, file_pin, pre-publish, pubsub_publish) is executed on the host side for efficiency.
5+
6+
## Usage
7+
8+
```rust
9+
use doc_sync::channel;
10+
11+
let cid = channel::post(document_bytes)?;
12+
```
13+
14+
## Architecture
15+
16+
The doc-sync module provides a simple API for publishing documents to IPFS via PubSub channels.
17+
All heavy operations are delegated to the host-side implementation for performance:
18+
19+
1. **file_add** - Add document to IPFS
20+
2. **file_pin** - Pin the document
21+
3. **pre-publish** - Prepare for PubSub
22+
4. **pubsub_publish** - Publish to the channel
23+
24+
## HTTP Gateway
25+
26+
The module exposes an HTTP endpoint for testing:
27+
28+
```bash
29+
curl -X POST http://localhost:5000/api/doc-sync/post \
30+
-H "Host: athena.hermes.local" \
31+
-H "Content-Type: text/plain" \
32+
-d "Hello, IPFS!"
33+
```
Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![allow(missing_docs)]
2-
//! Doc Sync Module
2+
//! Doc Sync - IPFS `PubSub` document publishing. See README.md for details.
33
44
shared::bindings_generate!({
55
world: "hermes:app/hermes",
@@ -11,40 +11,136 @@ shared::bindings_generate!({
1111
include wasi:cli/imports@0.2.6;
1212
import hermes:doc-sync/api;
1313
import hermes:logging/api;
14+
import hermes:http-gateway/api;
1415
1516
export hermes:init/event;
1617
export hermes:doc-sync/event;
18+
export hermes:http-gateway/event;
1719
}
1820
",
19-
share: ["hermes:logging", "hermes:doc-sync"],
21+
share: ["hermes:logging"],
2022
});
2123

2224
export!(Component);
2325

26+
use hermes::{
27+
doc_sync::api::{DocData, SyncChannel},
28+
http_gateway::api::{Bstr, Headers, HttpGatewayResponse, HttpResponse},
29+
};
2430
use shared::{
25-
bindings::hermes::doc_sync::api::{ChannelName, DocData, SyncChannel},
26-
utils::log::{self, info, warn},
31+
bindings::hermes::doc_sync::api::ChannelName,
32+
utils::log::{self, error, info},
2733
};
2834

29-
/// Doc Sync component.
35+
/// Doc Sync component - thin wrapper calling host-side implementation.
3036
struct Component;
3137

3238
impl exports::hermes::init::event::Guest for Component {
39+
/// Initialize the module.
3340
fn init() -> bool {
3441
log::init(log::LevelFilter::Trace);
35-
info!(target: "doc_sync::init", "💫 Opening channel...");
36-
let _chan = SyncChannel::new("documents");
37-
info!(target: "doc_sync::init", "💫 Channel opened");
42+
info!(target: "doc_sync::init", "Doc sync module initialized");
3843
true
3944
}
4045
}
4146

47+
/// Stub event handler for receiving documents (not used in this publishing-only demo).
4248
impl exports::hermes::doc_sync::event::Guest for Component {
4349
fn on_new_doc(
44-
channel: ChannelName,
45-
doc: DocData,
50+
_channel: ChannelName,
51+
_doc: DocData,
4652
) {
53+
// Not implemented - this demo only shows publishing
54+
}
55+
}
56+
57+
/// HTTP Gateway endpoint for testing with curl.
58+
///
59+
/// POST /api/doc-sync/post - Post a document to the "documents" channel
60+
///
61+
/// Example:
62+
/// ```bash
63+
/// curl -X POST http://localhost:5000/api/doc-sync/post \
64+
/// -H "Host: athena.hermes.local" \
65+
/// -H "Content-Type: text/plain" \
66+
/// -d "Hello, IPFS!"
67+
/// ```
68+
impl exports::hermes::http_gateway::event::Guest for Component {
69+
fn reply(
70+
body: Vec<u8>,
71+
_headers: Headers,
72+
path: String,
73+
method: String,
74+
) -> Option<HttpGatewayResponse> {
4775
log::init(log::LevelFilter::Trace);
48-
warn!(target: "doc_sync::on_new_doc", channel:%, doc_byte_length = doc.len(); "Unimplemented!");
76+
info!(target: "doc_sync", "HTTP {method} {path}");
77+
78+
match (method.as_str(), path.as_str()) {
79+
("POST", "/api/doc-sync/post") => {
80+
// Call channel::post (executes 4-step workflow on host)
81+
match channel::post(&body) {
82+
Ok(cid_bytes) => {
83+
let cid = String::from_utf8_lossy(&cid_bytes);
84+
Some(json_response(
85+
200,
86+
&serde_json::json!({
87+
"success": true,
88+
"cid": cid
89+
}),
90+
))
91+
},
92+
Err(e) => {
93+
error!(target: "doc_sync", "Failed to post document: {e:?}");
94+
Some(json_response(
95+
500,
96+
&serde_json::json!({
97+
"success": false,
98+
"error": "Failed to post document"
99+
}),
100+
))
101+
},
102+
}
103+
},
104+
_ => {
105+
Some(json_response(
106+
404,
107+
&serde_json::json!({"error": "Not found"}),
108+
))
109+
},
110+
}
111+
}
112+
}
113+
114+
/// Helper to create JSON HTTP responses.
115+
fn json_response(
116+
code: u16,
117+
body: &serde_json::Value,
118+
) -> HttpGatewayResponse {
119+
HttpGatewayResponse::Http(HttpResponse {
120+
code,
121+
headers: vec![("content-type".to_string(), vec![
122+
"application/json".to_string(),
123+
])],
124+
body: Bstr::from(body.to_string()),
125+
})
126+
}
127+
128+
/// Default channel name for doc-sync operations
129+
const DOC_SYNC_CHANNEL: &str = "documents";
130+
131+
/// API for posting documents to IPFS `PubSub` channels.
132+
pub mod channel {
133+
use super::{DOC_SYNC_CHANNEL, DocData, SyncChannel, hermes};
134+
135+
/// Post a document to the "documents" channel. Returns the document's CID.
136+
///
137+
/// # Errors
138+
///
139+
/// Returns an error if the document cannot be posted to the channel.
140+
pub fn post(document_bytes: &DocData) -> Result<Vec<u8>, hermes::doc_sync::api::Errno> {
141+
// Create channel via host
142+
let channel = SyncChannel::new(DOC_SYNC_CHANNEL);
143+
// Post document via host (executes 4-step workflow in host)
144+
channel.post(document_bytes)
49145
}
50146
}

hermes/bin/src/ipfs/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ pub fn bootstrap(
6060
let ipfs_node = HermesIpfsNode::init(
6161
HermesIpfsBuilder::new()
6262
.with_default()
63-
.set_default_listener()
6463
.set_disk_storage(ipfs_data_path.clone()),
6564
default_bootstrap,
6665
)?;
@@ -109,6 +108,11 @@ where N: hermes_ipfs::rust_ipfs::NetworkBehaviour<ToSwarm = Infallible> + Send +
109108
);
110109
}
111110
let hermes_node: HermesIpfs = node.into();
111+
// Enable DHT server mode for PubSub support
112+
hermes_node
113+
.dht_mode(hermes_ipfs::rust_ipfs::DhtMode::Server)
114+
.await?;
115+
tracing::debug!("IPFS node set to DHT server mode");
112116
let h = tokio::spawn(ipfs_command_handler(hermes_node, receiver));
113117
let (..) = tokio::join!(h);
114118
Ok::<(), anyhow::Error>(())

hermes/bin/src/ipfs/task.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,14 @@ pub(crate) async fn ipfs_command_handler(
9797
send_response(Ok(response), tx);
9898
},
9999
IpfsCommand::Publish(topic, message, tx) => {
100-
hermes_node
101-
.pubsub_publish(topic, message)
100+
let result = hermes_node
101+
.pubsub_publish(&topic, message)
102102
.await
103-
.map_err(|_| Errno::PubsubPublishError)?;
104-
send_response(Ok(()), tx);
103+
.map_err(|e| {
104+
tracing::error!(topic = %topic, "pubsub_publish failed: {}", e);
105+
Errno::PubsubPublishError
106+
});
107+
send_response(result, tx);
105108
},
106109
IpfsCommand::Subscribe(topic, tx) => {
107110
let stream = hermes_node

hermes/bin/src/logger.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub(crate) fn init(logger_config: &LoggerConfig) -> anyhow::Result<()> {
167167
.with_timer(time::UtcTime::rfc_3339())
168168
.with_span_events(FmtSpan::CLOSE)
169169
.with_max_level(LevelFilter::from_level(logger_config.log_level.into()))
170-
// Hardcode the filter to always suppress IPFS noise
170+
// Hardcode the filter to always suppress excess noise
171171
.with_env_filter(EnvFilter::new("hermes=info,rust_ipfs=error"))
172172
.finish();
173173

0 commit comments

Comments
 (0)