diff --git a/CHANGELOG-UNRELEASED.md b/CHANGELOG-UNRELEASED.md
index 2f730af61f..68ef5c410a 100644
--- a/CHANGELOG-UNRELEASED.md
+++ b/CHANGELOG-UNRELEASED.md
@@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added
+* Adds publishing of headers again after rollback. Header publishing is now its own action rather than part of the `Publish` action that plays nicely with the testing framework. It also adds header entries to the author list so they are gossiped properly. [#1640](https://github.com/holochain/holochain-rust/pull/1640).
+
### Changed
### Deprecated
diff --git a/app_spec/test/files/links.js b/app_spec/test/files/links.js
index c7a455bc10..7fdbba0bfb 100644
--- a/app_spec/test/files/links.js
+++ b/app_spec/test/files/links.js
@@ -43,23 +43,23 @@ module.exports = scenario => {
//bob expects zero links
t.ok(bob_agent_posts_expect_empty.Ok)
- t.equal(bob_agent_posts_expect_empty.Ok.links.length, 0);
- //alice expects zero alice
+ t.equal(bob_agent_posts_expect_empty.Ok.links.length, 0); // #!# fails with expected: 0 actual: 2
+ //alice expects zero links
t.ok(alice_agent_posts_expect_empty.Ok)
t.equal(alice_agent_posts_expect_empty.Ok.links.length, 0);
-
//different chain hash up to this point so we should be able to create a link with the same data
await alice.app.callSync("simple", "create_link",{ "base":alice.app.agentId, "target": "Posty" })
- //get alice posts
- const alice_posts_not_empty = await bob.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" })
+ //get posts as Alice and as Bob
+ const alice_posts_not_empty = await alice.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" })
+ const bob_posts_not_empty = await bob.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" })
//expect 1 post
t.ok(alice_posts_not_empty.Ok)
t.equal(alice_posts_not_empty.Ok.links.length, 1);
-
-
+ t.ok(bob_posts_not_empty.Ok)
+ t.equal(bob_posts_not_empty.Ok.links.length, 1); //#!# fails with expected: 1 actual: 2
})
diff --git a/core/src/action.rs b/core/src/action.rs
index 881d81abe6..2bf6e67e99 100644
--- a/core/src/action.rs
+++ b/core/src/action.rs
@@ -140,6 +140,10 @@ pub enum Action {
/// (only publish for AppEntryType, publish and publish_meta for links etc)
Publish(Address),
+ /// Publish to the network the header entry for the entry at the given address.
+ /// Note that the given address is that of the entry NOT the address of the header itself
+ PublishHeaderEntry(Address),
+
///Performs a Network Query Action based on the key and payload, used for links and Entries
Query((QueryKey, QueryPayload)),
diff --git a/core/src/agent/state.rs b/core/src/agent/state.rs
index ace1ddbafb..49b36074ad 100644
--- a/core/src/agent/state.rs
+++ b/core/src/agent/state.rs
@@ -421,7 +421,7 @@ pub mod tests {
let header = create_new_chain_header(
&test_entry(),
&agent_state,
- &StateWrapper::from(state.clone()),
+ &StateWrapper::from(state),
&None,
&vec![],
)
diff --git a/core/src/network/actions/mod.rs b/core/src/network/actions/mod.rs
index a82d33acf3..56210ef581 100644
--- a/core/src/network/actions/mod.rs
+++ b/core/src/network/actions/mod.rs
@@ -4,6 +4,7 @@ pub mod get_validation_package;
pub mod initialize_network;
pub mod publish;
pub mod shutdown;
+pub mod publish_header_entry;
use holochain_core_types::error::HcResult;
use holochain_persistence_api::cas::content::Address;
@@ -11,5 +12,6 @@ use holochain_persistence_api::cas::content::Address;
#[derive(Clone, Debug)]
pub enum ActionResponse {
Publish(HcResult
),
+ PublishHeaderEntry(HcResult),
Respond(HcResult<()>),
}
diff --git a/core/src/network/actions/publish_header_entry.rs b/core/src/network/actions/publish_header_entry.rs
new file mode 100644
index 0000000000..388f826714
--- /dev/null
+++ b/core/src/network/actions/publish_header_entry.rs
@@ -0,0 +1,54 @@
+use crate::{
+ action::{Action, ActionWrapper},
+ context::Context,
+ instance::dispatch_action,
+ network::actions::ActionResponse,
+};
+use futures::{future::Future, task::Poll};
+use holochain_core_types::error::HcResult;
+use holochain_persistence_api::cas::content::Address;
+use std::{pin::Pin, sync::Arc};
+
+/// Publish Header Entry Action Creator
+/// Returns a future that resolves to an ActionResponse.
+pub async fn publish_header_entry(address: Address, context: &Arc) -> HcResult {
+ let action_wrapper = ActionWrapper::new(Action::PublishHeaderEntry(address));
+ dispatch_action(context.action_channel(), action_wrapper.clone());
+ await!(PublishHeaderEntryFuture {
+ context: context.clone(),
+ action: action_wrapper,
+ })
+}
+
+/// PublishFuture resolves to ActionResponse
+/// Tracks the state for a response to its ActionWrapper
+pub struct PublishHeaderEntryFuture {
+ context: Arc,
+ action: ActionWrapper,
+}
+
+impl Future for PublishHeaderEntryFuture {
+ type Output = HcResult;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context) -> Poll {
+ if let Some(err) = self.context.action_channel_error("PublishHeaderEntryFuture") {
+ return Poll::Ready(Err(err));
+ }
+ let state = self.context.state().unwrap().network();
+ if let Err(error) = state.initialized() {
+ return Poll::Ready(Err(error));
+ }
+ //
+ // TODO: connect the waker to state updates for performance reasons
+ // See: https://github.com/holochain/holochain-rust/issues/314
+ //
+ cx.waker().clone().wake();
+ match state.actions().get(&self.action) {
+ Some(ActionResponse::PublishHeaderEntry(result)) => match result {
+ Ok(address) => Poll::Ready(Ok(address.to_owned())),
+ Err(error) => Poll::Ready(Err(error.clone())),
+ },
+ _ => Poll::Pending,
+ }
+ }
+}
diff --git a/core/src/network/handler/lists.rs b/core/src/network/handler/lists.rs
index 2859371bb5..972a24e837 100644
--- a/core/src/network/handler/lists.rs
+++ b/core/src/network/handler/lists.rs
@@ -5,11 +5,16 @@ use crate::{
instance::dispatch_action,
network::handler::{get_content_aspect, get_meta_aspects},
};
-use holochain_core_types::error::HcResult;
+use holochain_core_types::{
+ error::HcResult,
+ entry::Entry,
+};
use holochain_persistence_api::cas::content::{Address, AddressableContent};
use lib3h_protocol::data_types::{EntryListData, GetListData};
use snowflake::ProcessUniqueId;
use std::{collections::HashMap, sync::Arc, thread};
+use crate::network::entry_aspect::EntryAspect;
+use crate::agent::state::create_new_chain_header;
pub fn handle_get_authoring_list(get_list_data: GetListData, context: Arc) {
thread::Builder::new()
@@ -20,11 +25,32 @@ pub fn handle_get_authoring_list(get_list_data: GetListData, context: Arc) -> Vec {
- let chain = context.state().unwrap().agent().chain_store();
- let top_header = context.state().unwrap().agent().top_chain_header();
+ let chain = context.state().unwrap().agent().iter_chain();
chain
- .iter(&top_header)
.filter(|ref chain_header| chain_header.entry_type().can_publish(&context))
.map(|chain_header| chain_header.entry_address().clone())
.collect()
}
+fn get_all_chain_header_entries(context: Arc) -> Vec {
+ let chain = context.state().unwrap().agent().iter_chain();
+ chain
+ .map(|chain_header| Entry::ChainHeader(chain_header))
+ .collect()
+}
+
fn get_all_aspect_addresses(entry: &Address, context: Arc) -> HcResult> {
let mut address_list: Vec = get_meta_aspects(entry, context.clone())?
.iter()
@@ -90,3 +121,67 @@ pub fn handle_get_gossip_list(get_list_data: GetListData, context: Arc)
})
.expect("Could not spawn thread for creating of gossip list");
}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::workflows::author_entry::author_entry;
+ use crate::nucleus::actions::tests::*;
+ use holochain_core_types::{
+ entry::{Entry, test_entry_with_value},
+ };
+ use holochain_persistence_api::cas::content::AddressableContent;
+ use std::{thread, time};
+
+ #[test]
+ fn test_can_get_chain_header_list() {
+ let mut dna = test_dna();
+ dna.uuid = "test_can_get_chain_header_list".to_string();
+ let (_instance, context) = instance_by_name("jill", dna, None);
+
+ context
+ .block_on(author_entry(
+ &test_entry_with_value("{\"stuff\":\"test entry value\"}"),
+ None,
+ &context,
+ &vec![],
+ ))
+ .unwrap()
+ .address();
+
+ thread::sleep(time::Duration::from_millis(500));
+
+ let chain = context.state().unwrap().agent().iter_chain();
+ let header_entries: Vec = chain.map(|header| Entry::ChainHeader(header)).collect();
+
+ assert_eq!(
+ get_all_chain_header_entries(context),
+ header_entries,
+ )
+
+ }
+
+ #[test]
+ fn test_can_get_all_aspect_addr_for_headers() {
+ let mut dna = test_dna();
+ dna.uuid = "test_can_get_chain_header_list".to_string();
+ let (_instance, context) = instance_by_name("jill", dna, None);
+
+ context
+ .block_on(author_entry(
+ &test_entry_with_value("{\"stuff\":\"test entry value\"}"),
+ None,
+ &context,
+ &vec![],
+ ))
+ .unwrap()
+ .address();
+
+ thread::sleep(time::Duration::from_millis(500));
+
+ assert!(get_all_chain_header_entries(context.clone()).iter().all(|chain_header| {
+ get_all_aspect_addresses(&chain_header.address(), context.clone()).is_ok()
+ }));
+ }
+
+}
diff --git a/core/src/network/handler/mod.rs b/core/src/network/handler/mod.rs
index 6c134d0802..98caffbd39 100644
--- a/core/src/network/handler/mod.rs
+++ b/core/src/network/handler/mod.rs
@@ -18,7 +18,6 @@ use crate::{
store::*,
},
},
- nucleus,
workflows::get_entry_result::get_entry_with_meta_workflow,
};
use boolinator::*;
@@ -31,6 +30,8 @@ use lib3h_protocol::{
protocol_server::Lib3hServerProtocol,
};
use std::{convert::TryFrom, sync::Arc};
+use crate::nucleus::actions::get_entry::get_entry_from_cas;
+use crate::network::entry_with_header::EntryWithHeader;
// FIXME: Temporary hack to ignore messages incorrectly sent to us by the networking
// module that aren't really meant for us
@@ -252,35 +253,67 @@ fn get_content_aspect(
entry_address: &Address,
context: Arc,
) -> Result {
- let entry_with_meta =
- nucleus::actions::get_entry::get_entry_with_meta(&context, entry_address.clone())?
- .ok_or(HolochainError::EntryNotFoundLocally)?;
+ let state = context.state()
+ .ok_or_else(|| {
+ HolochainError::InitializationFailed(
+ String::from("In get_content_aspect: no state found")
+ )
+ })?;
+
+ // Optimistically look for entry in chain...
+ let maybe_chain_header = state.agent()
+ .iter_chain()
+ .find(|ref chain_header| chain_header.entry_address() == entry_address);
+
+ // If we have found a header for the requested entry in the chain...
+ let maybe_entry_with_header = if let Some(header) = maybe_chain_header {
+ // ... we can just get the content from the chain CAS
+ Some(EntryWithHeader {
+ entry: get_entry_from_cas(&state.agent().chain_store().content_storage(), header.entry_address())?
+ .expect("Could not find entry in chain CAS, but header is chain"),
+ header
+ })
+ } else {
+ // ... but if we didn't author that entry, let's see if we have it in the DHT cas:
+ if let Some(entry) = get_entry_from_cas(&state.dht().content_storage(), entry_address)? {
+ // If we have it in the DHT cas that's good,
+ // but then we have to get the header like this:
+ let headers = context
+ .state()
+ .expect("Could not get state for handle_fetch_entry")
+ .get_headers(entry_address.clone())
+ .map_err(|error| {
+ let err_message = format!(
+ "net/fetch/get_content_aspect: Error trying to get headers {:?}",
+ error
+ );
+ log_error!(context, "{}", err_message.clone());
+ HolochainError::ErrorGeneric(err_message)
+ })?;
+ if headers.len() > 0 {
+ // TODO: this is just taking the first header..
+ // We should actually transform all headers into EntryAspect::Headers and just the first one
+ // into an EntryAspect content (What about ordering? Using the headers timestamp?)
+ Some(EntryWithHeader{entry, header: headers[0].clone()})
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ };
+
+ let entry_with_header = maybe_entry_with_header.ok_or(HolochainError::EntryNotFoundLocally)?;
- let _ = entry_with_meta
+ let _ = entry_with_header
.entry
.entry_type()
.can_publish(&context)
.ok_or(HolochainError::EntryIsPrivate)?;
- let headers = context
- .state()
- .expect("Could not get state for handle_fetch_entry")
- .get_headers(entry_address.clone())
- .map_err(|error| {
- let err_message = format!(
- "net/fetch/get_content_aspect: Error trying to get headers {:?}",
- error
- );
- log_error!(context, "{}", err_message.clone());
- HolochainError::ErrorGeneric(err_message)
- })?;
-
- // TODO: this is just taking the first header..
- // We should actually transform all headers into EntryAspect::Headers and just the first one
- // into an EntryAspect content (What about ordering? Using the headers timestamp?)
Ok(EntryAspect::Content(
- entry_with_meta.entry,
- headers[0].clone(),
+ entry_with_header.entry,
+ entry_with_header.header,
))
}
diff --git a/core/src/network/reducers/mod.rs b/core/src/network/reducers/mod.rs
index 4b17cc5581..d51605518f 100644
--- a/core/src/network/reducers/mod.rs
+++ b/core/src/network/reducers/mod.rs
@@ -5,6 +5,7 @@ pub mod handle_get_result;
pub mod handle_get_validation_package;
pub mod init;
pub mod publish;
+pub mod publish_header_entry;
pub mod resolve_direct_connection;
pub mod respond_authoring_list;
pub mod respond_fetch;
@@ -25,6 +26,7 @@ use crate::{
handle_get_validation_package::reduce_handle_get_validation_package,
init::reduce_init,
publish::reduce_publish,
+ publish_header_entry::reduce_publish_header_entry,
resolve_direct_connection::reduce_resolve_direct_connection,
respond_authoring_list::reduce_respond_authoring_list,
respond_fetch::reduce_respond_fetch_data,
@@ -58,6 +60,7 @@ fn resolve_reducer(action_wrapper: &ActionWrapper) -> Option {
Action::HandleGetValidationPackage(_) => Some(reduce_handle_get_validation_package),
Action::InitNetwork(_) => Some(reduce_init),
Action::Publish(_) => Some(reduce_publish),
+ Action::PublishHeaderEntry(_) => Some(reduce_publish_header_entry),
Action::ResolveDirectConnection(_) => Some(reduce_resolve_direct_connection),
Action::RespondAuthoringList(_) => Some(reduce_respond_authoring_list),
Action::RespondGossipList(_) => Some(reduce_respond_gossip_list),
diff --git a/core/src/network/reducers/publish.rs b/core/src/network/reducers/publish.rs
index ee43398292..f1c5f4a7ad 100644
--- a/core/src/network/reducers/publish.rs
+++ b/core/src/network/reducers/publish.rs
@@ -127,6 +127,7 @@ fn reduce_publish_inner(
network_state.initialized()?;
let entry_with_header = fetch_entry_with_header(&address, root_state)?;
+
match entry_with_header.entry.entry_type() {
EntryType::AgentId => publish_entry(network_state, &entry_with_header),
EntryType::App(_) => publish_entry(network_state, &entry_with_header).and_then(|_| {
@@ -156,7 +157,7 @@ fn reduce_publish_inner(
}
}),
_ => Err(HolochainError::NotImplemented(
- "reduce_publish_inner".into(),
+ format!("reduce_publish_inner not implemented for {}", entry_with_header.entry.entry_type()),
)),
}
}
diff --git a/core/src/network/reducers/publish_header_entry.rs b/core/src/network/reducers/publish_header_entry.rs
new file mode 100644
index 0000000000..97132d763c
--- /dev/null
+++ b/core/src/network/reducers/publish_header_entry.rs
@@ -0,0 +1,110 @@
+use crate::{
+ action::ActionWrapper,
+ network::{
+ actions::ActionResponse,
+ entry_aspect::EntryAspect,
+ entry_with_header::{fetch_entry_with_header},
+ reducers::send,
+ state::NetworkState,
+ },
+ state::State,
+ agent::state::create_new_chain_header,
+};
+use holochain_core_types::{
+ entry::{Entry},
+ error::HolochainError,
+ chain_header::ChainHeader,
+};
+use lib3h_protocol::{
+ data_types::{EntryData, ProvidedEntryData},
+ protocol_client::Lib3hClientProtocol,
+};
+
+use holochain_persistence_api::cas::content::{Address, AddressableContent};
+use crate::state::StateWrapper;
+
+
+/// Send to network a request to publish a header entry alone
+/// This is similar to publishing a regular entry but it has its own special dummy header.
+fn publish_header(
+ network_state: &mut NetworkState,
+ root_state: &State,
+ chain_header: &ChainHeader,
+) -> Result<(), HolochainError> {
+ let header_entry = Entry::ChainHeader(chain_header.clone());
+ let header_entry_header = create_new_chain_header(
+ &header_entry,
+ &root_state.agent(),
+ &StateWrapper::from(root_state.clone()),
+ &None,
+ &Vec::new(),
+ )?;
+ send(
+ network_state,
+ Lib3hClientProtocol::PublishEntry(ProvidedEntryData {
+ space_address: network_state.dna_address.clone().unwrap(),
+ provider_agent_id: network_state.agent_id.clone().unwrap().into(),
+ entry: EntryData {
+ entry_address: header_entry.address().clone(),
+ aspect_list: vec![EntryAspect::Content(
+ header_entry.clone(),
+ header_entry_header,
+ )
+ .into()],
+ },
+ }),
+ )
+}
+
+
+fn reduce_publish_header_entry_inner(
+ network_state: &mut NetworkState,
+ root_state: &State,
+ address: &Address,
+) -> Result<(), HolochainError> {
+ network_state.initialized()?;
+ let entry_with_header = fetch_entry_with_header(&address, root_state)?;
+ publish_header(network_state, root_state, &entry_with_header.header)
+}
+
+pub fn reduce_publish_header_entry(
+ network_state: &mut NetworkState,
+ root_state: &State,
+ action_wrapper: &ActionWrapper,
+) {
+ let action = action_wrapper.action();
+ let address = unwrap_to!(action => crate::action::Action::PublishHeaderEntry);
+
+ let result = reduce_publish_header_entry_inner(network_state, root_state, &address);
+ network_state.actions.insert(
+ action_wrapper.clone(),
+ ActionResponse::PublishHeaderEntry(match result {
+ Ok(_) => Ok(address.clone()),
+ Err(e) => Err(HolochainError::ErrorGeneric(e.to_string())),
+ }),
+ );
+}
+
+#[cfg(test)]
+mod tests {
+
+ use crate::{
+ action::{Action, ActionWrapper},
+ instance::tests::test_context,
+ state::test_store,
+ };
+ use holochain_core_types::entry::test_entry;
+ use holochain_persistence_api::cas::content::AddressableContent;
+
+ #[test]
+ pub fn reduce_publish_header_entry_test() {
+ let context = test_context("alice", None);
+ let store = test_store(context.clone());
+
+ let entry = test_entry();
+ let action_wrapper = ActionWrapper::new(Action::PublishHeaderEntry(entry.address()));
+
+ store.reduce(action_wrapper);
+ }
+
+}
diff --git a/core/src/nucleus/actions/build_validation_package.rs b/core/src/nucleus/actions/build_validation_package.rs
index 8d5f2fc9e4..6e87cac3fe 100644
--- a/core/src/nucleus/actions/build_validation_package.rs
+++ b/core/src/nucleus/actions/build_validation_package.rs
@@ -6,6 +6,7 @@ use crate::{
nucleus::ribosome::callback::{
validation_package::get_validation_package_definition, CallbackResult,
},
+ state::State,
};
use futures::{future::Future, task::Poll};
use holochain_core_types::{
@@ -18,6 +19,7 @@ use holochain_core_types::{
};
use snowflake;
use std::{convert::TryInto, pin::Pin, sync::Arc, thread, vec::Vec};
+use crate::state::StateWrapper;
pub async fn build_validation_package<'a>(
entry: &'a Entry,
@@ -88,11 +90,11 @@ pub async fn build_validation_package<'a>(
// and just used for the validation, I don't see why it would be a problem.
// If it was a problem, we would have to make sure that the whole commit process
// (including validtion) is atomic.
- let state = &context.state()?;
+ let state = State::new(context.clone());
agent::state::create_new_chain_header(
&entry,
- &state.agent(),
- &*state,
+ &context.state()?.agent(),
+ &StateWrapper::from(state),
&None,
provenances,
)?
diff --git a/core/src/nucleus/actions/get_entry.rs b/core/src/nucleus/actions/get_entry.rs
index 01b48bf816..3f98b20cdb 100644
--- a/core/src/nucleus/actions/get_entry.rs
+++ b/core/src/nucleus/actions/get_entry.rs
@@ -7,13 +7,12 @@ use holochain_core_types::{
};
use holochain_persistence_api::{
- cas::{content::Address, storage::ContentAddressableStorage},
+ cas::{content::{Address, AddressableContent}, storage::ContentAddressableStorage},
eav::IndexFilter,
};
use std::{
collections::BTreeSet,
- convert::TryInto,
str::FromStr,
sync::{Arc, RwLock},
};
@@ -22,10 +21,12 @@ pub(crate) fn get_entry_from_cas(
storage: &Arc>,
address: &Address,
) -> Result