fix: allow multiple subscriptions per nuri

feat/orm
Laurin Weger 2 weeks ago
parent 6ed1359887
commit 3d8e4af2ee
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 229
      ng-verifier/src/orm.rs
  2. 143
      ng-verifier/src/utils/orm_validation.rs
  3. 2
      ng-verifier/src/verifier.rs

@ -8,16 +8,18 @@
// according to those terms.
use futures::channel::mpsc;
use ng_net::actors::app::session;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::u64;
use futures::SinkExt;
use lazy_static::lazy_static;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_net::utils::Receiver;
use ng_net::{app_protocol::*, orm::*};
pub use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_oxigraph::oxigraph::sparql::{Query, QueryResults};
use ng_oxigraph::oxrdf::Triple;
use ng_repo::errors::NgError;
@ -77,36 +79,53 @@ impl Verifier {
}
}
fn process_changes_for_subscription(
/// Helper to call process_changes_for_shape for all subscriptions on nuri's document.
fn process_changes_for_nuri_and_session(
self: &mut Self,
nuri: &NuriV0,
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple]
triples_removed: &[Triple],
) -> Result<OrmChanges, NgError> {
let orm_subscription = self.orm_subscriptions.get(nuri).unwrap();
//let tracked_subjects = orm_subscription.tracked_subjects;
let schema = &orm_subscription.shape_type.schema;
let root_shape = schema
.get(&orm_subscription.shape_type.shape)
.ok_or(VerifierError::InvalidOrmSchema)?;
let mut orm_changes = HashMap::new();
self.process_changes_for_subject_and_shape(
root_shape.clone(),
let shapes: Vec<_> = self
.orm_subscriptions
.get(nuri)
.unwrap()
.iter()
.map(|s| {
s.shape_type
.schema
.get(&s.shape_type.shape)
.unwrap()
.clone()
})
.collect();
for root_shape in shapes {
self.process_changes_for_shape_and_session(
nuri,
root_shape,
session_id,
triples_added,
triples_removed,
nuri,
&mut orm_changes,
)?;
}
Ok(orm_changes)
}
fn process_changes_for_subject_and_shape(
/// Add and remove the triples from the tracked subjects,
/// re-validate, and update `changes` containing the updated data.
fn process_changes_for_shape_and_session(
self: &mut Self,
nuri: &NuriV0,
root_shape: Arc<OrmSchemaShape>,
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple],
nuri: &NuriV0,
orm_changes: &mut OrmChanges,
) -> Result<(), NgError> {
let nuri_repo = nuri.repo();
@ -161,7 +180,14 @@ impl Verifier {
// Apply all triples for that subject to the tracked (shape, subject) pair.
// Record the changes.
{
let orm_subscription = self.orm_subscriptions.get_mut(nuri).unwrap();
let orm_subscription = self
.orm_subscriptions
.get_mut(nuri)
.unwrap()
.iter()
.find(|s| s.session_id == session_id && s.shape_type.shape == shape.iri)
.unwrap();
if let Err(e) = add_remove_triples_mut(
shape.clone(),
subject_iri,
@ -173,9 +199,9 @@ impl Verifier {
log_err!("apply_changes_from_triples add/remove error: {:?}", e);
}
let validity = {
let tracked_subject_opt = orm_subscription.tracked_subjects
let tracked_subject_opt = orm_subscription
.tracked_subjects
.get(subject_iri)
.and_then(|m| m.get(&shape.iri));
let Some(tracked_subject) = tracked_subject_opt else {
@ -183,9 +209,14 @@ impl Verifier {
}; // skip if missing
tracked_subject.valid.clone()
};
// Validate the subject.
let need_eval =
update_subject_validity(change, &shape, &mut orm_subscription.tracked_subjects, validity);
let need_eval = update_subject_validity(
change,
&shape,
&mut orm_subscription.tracked_subjects,
validity,
);
// We add the need_eval to be processed next after loop.
for (iri, schema_shape, needs_refetch) in need_eval {
@ -198,9 +229,6 @@ impl Verifier {
}
}
let orm_subscription = self.orm_subscriptions.get(nuri).unwrap();
let schema = &orm_subscription.shape_type.schema;
// Now, we fetch all un-fetched subjects for re-evaluation.
for (shape_iri, objects_to_eval) in &nested_objects_to_eval {
let objects_to_fetch = objects_to_eval
@ -208,25 +236,35 @@ impl Verifier {
.filter(|(_iri, needs_fetch)| *needs_fetch)
.map(|(s, _)| s.clone())
.collect();
let shape = schema
let orm_subscription = self
.get_orm_subscriptions_for(nuri, Some(&shape.iri), Some(&session_id))
.get(0)
.unwrap();
let schema = &orm_subscription.shape_type.schema;
let shape: &Arc<OrmSchemaShape> = schema
.get(shape_iri)
.ok_or(VerifierError::InvalidOrmSchema)?;
// Create sparql query
let shape_query =
shape_type_to_sparql(&schema, &shape_iri, Some(objects_to_fetch))?;
let new_triples = self.query_sparql_construct(shape_query, Some(nuri_repo.clone()))?;
// self.process_changes_for_subject_and_shape(
// shape.clone(),
// &new_triples,
// &vec![],
// nuri,
// orm_changes,
// )?;
}
// Now, add all subjects to the queue that did not need a fetch.
for (shape_iri, objects_to_eval) in &nested_objects_to_eval {
let objects_to_fetch = objects_to_eval
let new_triples =
self.query_sparql_construct(shape_query, Some(nuri_repo.clone()))?;
self.process_changes_for_shape_and_session(
nuri,
shape.clone(),
session_id,
&new_triples,
&vec![],
orm_changes,
)?;
// @Niko, if I put this in the same loop, I get borrow conflicts
let objects_not_to_fetch = objects_to_eval
.iter()
.filter(|(_iri, needs_fetch)| !*needs_fetch)
.map(|(s, _)| s.clone())
@ -235,54 +273,70 @@ impl Verifier {
.get(shape_iri)
.ok_or(VerifierError::InvalidOrmSchema)?;
shape_validation_queue.push((shape.clone(), objects_to_fetch));
shape_validation_queue.push((shape.clone(), objects_not_to_fetch));
}
}
Ok(())
}
/// Helper to get orm subscriptions for nuri, shapes and sessions.
fn get_orm_subscriptions_for(
&self,
nuri: &NuriV0,
shape: Option<&ShapeIri>,
session_id: Option<&u64>,
) -> Vec<&OrmSubscription> {
self.orm_subscriptions.get(nuri).unwrap().
// Filter shapes, if present.
iter().filter(|s| match shape {
Some(sh) => *sh == s.shape_type.shape,
None => true
// Filter session ids if present.
}).filter(|s| match session_id {
Some(id) => *id == s.session_id,
None => true
}).collect()
}
/// Apply triples to a nuri's document.
/// Updates tracked_subjects in orm_subscriptions.
fn apply_triple_changes(
&mut self,
triples_added: &[Triple],
triples_removed: &[Triple],
only_for_nuri: Option<&NuriV0>,
nuri: &NuriV0,
only_for_session_id: Option<u64>,
) -> Result<OrmChanges, NgError> {
// If we have a specific session, handle only that subscription.
if let Some(session_id) = only_for_session_id {
if let Some((nuri, sub)) = self
.orm_subscriptions
.iter()
.find(|(_, s)| s.session_id == session_id)
{
// TODO: Is repo correct?
return self.process_changes_for_subscription(
return self.process_changes_for_nuri_and_session(
&nuri.clone(),
session_id,
triples_added,
triples_removed
triples_removed,
);
} else {
return Ok(HashMap::new());
}
}
// Otherwise, iterate all (optionally filter by nuri) and merge.
// Otherwise, iterate all sessions.
let mut merged: OrmChanges = HashMap::new();
for nuri in self.orm_subscriptions.iter().filter(|(nuri,_)|{
if let Some(filter_nuri) = only_for_nuri {
if *nuri != filter_nuri {
return false;
}
}
true
}).map(|(nuri,_)| nuri.clone()).collect::<Vec<_>>() {
let changes = self.process_changes_for_subscription(
let session_ids: Vec<_> = self
.orm_subscriptions
.get(nuri)
.unwrap()
.iter()
.map(|s| s.session_id.clone())
.collect();
for session_id in session_ids {
let changes = self.process_changes_for_nuri_and_session(
&nuri,
session_id,
triples_added,
triples_removed,
)?;
for (shape_iri, subj_map) in changes {
merged
.entry(shape_iri)
@ -293,6 +347,7 @@ impl Verifier {
Ok(merged)
}
/// Create ORM JSON object from OrmTrackedSubjectChange and shape.
fn materialize_orm_object(
change: &OrmTrackedSubjectChange,
changes: &OrmChanges,
@ -414,23 +469,29 @@ impl Verifier {
return orm_obj;
}
fn create_orm_from_triples(
/// For a nuri, session, and shape, create an ORM JSON object.
fn create_orm_object_for_shape(
&mut self,
nuri: &NuriV0,
triples: &[Triple],
session_id: u64,
shape_type: &OrmShapeType,
) -> Result<Value, NgError> {
let session_id = {
let orm_subscription = self.orm_subscriptions.get(nuri).unwrap();
orm_subscription.session_id
};
// Query triples for this shape
let shape_query = shape_type_to_sparql(&shape_type.schema, &shape_type.shape, None)?;
// TODO: How to stringify nuri correctly?
let shape_triples = self.query_sparql_construct(shape_query, Some(nuri.repo()))?;
let changes: OrmChanges =
self.apply_triple_changes(triples, &[], None, Some(session_id))?;
self.apply_triple_changes(&shape_triples, &[], nuri, Some(session_id.clone()))?;
let orm_subscription = *self
.get_orm_subscriptions_for(nuri, Some(&shape_type.shape), Some(&session_id))
.get(0)
.unwrap();
let orm_subscription = self.orm_subscriptions.get(nuri).unwrap();
let root_shape_iri = &orm_subscription.shape_type.shape;
let schema = &orm_subscription.shape_type.schema;
let root_shape = schema
.get(root_shape_iri)
.get(&shape_type.shape)
.ok_or(VerifierError::InvalidOrmSchema)?;
let Some(_root_changes) = changes.get(&root_shape.iri).map(|s| s.values()) else {
return Ok(Value::Array(vec![]));
@ -442,11 +503,11 @@ impl Verifier {
// For each valid change struct, we build an orm object.
// The way we get the changes from the tracked subjects is a bit hacky, sorry.
for (subject_iri, tracked_subjects_by_shape) in &orm_subscription.tracked_subjects {
if let Some(tracked_subject) = tracked_subjects_by_shape.get(root_shape_iri) {
if let Some(tracked_subject) = tracked_subjects_by_shape.get(&shape_type.shape) {
if tracked_subject.valid == OrmTrackedSubjectValidity::Valid {
if let Some(change) = changes
.get(root_shape_iri)
.and_then(|subject_iri_to_ts| subject_iri_to_ts.get(subject_iri))
.get(&shape_type.shape)
.and_then(|subject_iri_to_ts| subject_iri_to_ts.get(subject_iri).clone())
{
let new_val = Self::materialize_orm_object(
change,
@ -463,7 +524,7 @@ impl Verifier {
return Ok(return_vals);
}
pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphQuadsPatch) {}
pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphTransaction) {}
pub(crate) async fn orm_frontend_update(
&mut self,
@ -495,12 +556,14 @@ impl Verifier {
}
pub(crate) fn clean_orm_subscriptions(&mut self) {
self.orm_subscriptions.retain(|_,subscription|
!subscription.sender.is_closed()
);
self.orm_subscriptions.retain(|_, subscriptions| {
subscriptions.retain(|sub| !sub.sender.is_closed());
!subscriptions.is_empty()
});
}
/// Entry point to create a new orm subscription.
/// Triggers the creation of an orm object which is sent back to the receiver.
pub(crate) async fn start_orm(
&mut self,
nuri: &NuriV0,
@ -522,15 +585,11 @@ impl Verifier {
nuri: nuri.clone(),
};
self.orm_subscriptions
.insert(nuri.clone(), orm_subscription);
// Query triples for this shape
let shape_query = shape_type_to_sparql(&shape_type.schema, &shape_type.shape, None)?;
// TODO: How to stringify nuri correctly?
let shape_triples = self.query_sparql_construct(shape_query, Some(nuri.repo()))?;
.entry(nuri.clone())
.or_insert(vec![])
.push(orm_subscription);
// Create objects from queried triples.
let _orm_objects = self.create_orm_from_triples(nuri, &shape_triples)?;
let orm_objects = self.create_orm_object_for_shape(nuri, session_id, &shape_type);
// TODO integrate response

@ -9,8 +9,8 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Weak;
use std::sync::Arc;
use std::sync::Weak;
use ng_net::orm::*;
use ng_oxigraph::oxrdf::Subject;
@ -23,11 +23,8 @@ pub fn group_by_subject_for_shape<'a>(
allowed_subjects: &[String],
) -> HashMap<String, Vec<&'a Triple>> {
let mut triples_by_subject: HashMap<String, Vec<&Triple>> = HashMap::new();
let allowed_preds_set: HashSet<&str> = shape
.predicates
.iter()
.map(|p| p.iri.as_str())
.collect();
let allowed_preds_set: HashSet<&str> =
shape.predicates.iter().map(|p| p.iri.as_str()).collect();
let allowed_subject_set: HashSet<&str> = allowed_subjects.iter().map(|s| s.as_str()).collect();
for triple in triples {
// triple.subject must be in allowed_subjects (or allowed_subjects empty)
@ -62,30 +59,32 @@ pub fn add_remove_triples_mut(
tracked_subjects: &mut HashMap<String, HashMap<String, Arc<OrmTrackedSubject>>>,
subject_changes: &mut OrmTrackedSubjectChange,
) -> Result<(), NgError> {
fn get_or_create_tracked_subject<'a> (
fn get_or_create_tracked_subject<'a>(
subject_iri: &str,
shape: &Arc<OrmSchemaShape>,
tracked_subjects: &'a mut HashMap<String, HashMap<String, Arc<OrmTrackedSubject>>>)
-> (&'a mut OrmTrackedSubject, Weak<OrmTrackedSubject>)
{
tracked_subjects: &'a mut HashMap<String, HashMap<String, Arc<OrmTrackedSubject>>>,
) -> (&'a mut OrmTrackedSubject, Weak<OrmTrackedSubject>) {
let tracked_shapes_for_subject = tracked_subjects
.entry(subject_iri.to_string())
.or_insert_with(HashMap::new);
let subject = tracked_shapes_for_subject
.entry(shape.iri.clone())
.or_insert_with(|| Arc::new(OrmTrackedSubject {
.or_insert_with(|| {
Arc::new(OrmTrackedSubject {
tracked_predicates: HashMap::new(),
parents: HashMap::new(),
valid: ng_net::orm::OrmTrackedSubjectValidity::Pending,
subject_iri: subject_iri.to_string(),
shape: shape.clone(),
}));
})
});
let weak = Arc::downgrade(&subject);
(Arc::get_mut(subject).unwrap(), weak)
}
let (_, tracked_subject_weak) = get_or_create_tracked_subject(subject_iri, &shape, tracked_subjects);
let (_, tracked_subject_weak) =
get_or_create_tracked_subject(subject_iri, &shape, tracked_subjects);
// Process added triples.
// For each triple, check if it matches the shape.
@ -101,17 +100,19 @@ pub fn add_remove_triples_mut(
let mut upgraded = tracked_subject_weak.upgrade().unwrap();
let tracked_subject = Arc::get_mut(&mut upgraded).unwrap();
// Add tracked predicate or increase cardinality
let _tracked_predicate = tracked_subject
let tracked_predicate_ = tracked_subject
.tracked_predicates
.entry(predicate_schema.iri.to_string())
.or_insert_with(|| Arc::new(OrmTrackedPredicate {
.or_insert_with(|| {
Arc::new(OrmTrackedPredicate {
current_cardinality: 0,
schema: predicate_schema.clone(),
tracked_children: Vec::new(),
current_literals: None,
}));
let tracked_predicate_weak = Arc::downgrade(&_tracked_predicate);
let tracked_predicate = Arc::get_mut(_tracked_predicate).unwrap();
})
});
let tracked_predicate_weak = Arc::downgrade(&tracked_predicate_);
let tracked_predicate = Arc::get_mut(tracked_predicate_).unwrap();
tracked_predicate.current_cardinality += 1;
let obj_term = oxrdf_term_to_orm_basic_type(&triple.object);
@ -146,25 +147,30 @@ pub fn add_remove_triples_mut(
// If predicate is of type shape, register (parent -> child) links so that
// nested subjects can later be (lazily) fetched / validated.
// FIXME : shape_iri is never used
for shape_iri in predicate_schema
.dataTypes
.iter()
.filter_map(|dt| if dt.valType == OrmSchemaLiteralType::shape {dt.shape.clone()} else {None} )
{
for shape_iri in predicate_schema.dataTypes.iter().filter_map(|dt| {
if dt.valType == OrmSchemaLiteralType::shape {
dt.shape.clone()
} else {
None
}
}) {
if let BasicType::Str(obj_iri) = &obj_term {
// Get or create object's tracked subject struct.
let (tracked_child, tracked_child_weak) =
get_or_create_tracked_subject(triple.predicate.as_string(), &shape, tracked_subjects);
let (tracked_child, tracked_child_weak) = get_or_create_tracked_subject(
triple.predicate.as_string(),
&shape,
tracked_subjects,
);
// Add self to parent (set tracked to true, preliminary).
tracked_child.parents.insert(obj_iri.clone(), tracked_child_weak.clone());
tracked_child
.parents
.insert(obj_iri.clone(), tracked_child_weak.clone());
// Add link to children
let mut upgraded = tracked_predicate_weak.upgrade().unwrap();
let tracked_predicate = Arc::get_mut(&mut upgraded).unwrap();
tracked_predicate
.tracked_children
.push(tracked_child_weak);
tracked_predicate.tracked_children.push(tracked_child_weak);
}
}
}
@ -177,7 +183,12 @@ pub fn add_remove_triples_mut(
let tracked_predicate_opt = tracked_subjects
.get_mut(subject_iri)
.and_then(|tss| tss.get_mut(&shape.iri))
.and_then(|ts| Arc::get_mut(ts).unwrap().tracked_predicates.get_mut(pred_iri));
.and_then(|ts| {
Arc::get_mut(ts)
.unwrap()
.tracked_predicates
.get_mut(pred_iri)
});
let Some(tracked_predicate_arc) = tracked_predicate_opt else {
continue;
};
@ -214,12 +225,13 @@ pub fn add_remove_triples_mut(
.any(|dt| dt.valType == OrmSchemaLiteralType::shape)
{
// Remove parent from child and child from tracked children.
for shape_iri in tracked_predicate
.schema
.dataTypes
.iter()
.filter_map(|dt| if dt.valType == OrmSchemaLiteralType::shape {dt.shape.clone()} else {None} )
{
for shape_iri in tracked_predicate.schema.dataTypes.iter().filter_map(|dt| {
if dt.valType == OrmSchemaLiteralType::shape {
dt.shape.clone()
} else {
None
}
}) {
// Nested shape removal logic disabled (see note above).
}
}
@ -248,13 +260,16 @@ pub fn update_subject_validity(
// Check 1) Check if we need to fetch this object or all parents are untracked.
if tracked_subject.parents.len() != 0 {
let no_parents_tracking = tracked_subject.parents.values().all(|parent| {
match parent.upgrade() {
Some(subject) =>
let no_parents_tracking =
tracked_subject
.parents
.values()
.all(|parent| match parent.upgrade() {
Some(subject) => {
subject.valid == OrmTrackedSubjectValidity::Untracked
|| subject.valid == OrmTrackedSubjectValidity::Invalid,
None => true
|| subject.valid == OrmTrackedSubjectValidity::Invalid
}
None => true,
});
if no_parents_tracking {
@ -300,8 +315,9 @@ pub fn update_subject_validity(
let p_change = s_change.predicates.get(&p_schema.iri);
let tracked_pred = p_change.and_then(|pc| pc.tracked_predicate.upgrade());
let count =
tracked_pred.as_ref().map_or_else(|| 0, |tp| tp.current_cardinality);
let count = tracked_pred
.as_ref()
.map_or_else(|| 0, |tp| tp.current_cardinality);
// Check 4.1) Cardinality
if count < p_schema.minCardinality {
@ -345,10 +361,11 @@ pub fn update_subject_validity(
// Check that each required literal is present.
for required_literal in required_literals {
// Is tracked predicate present?
if !tracked_pred.as_ref().map_or(false,
|t|t.current_literals.as_ref().map_or(false,
|tt|tt.iter().any(|literal| *literal == *required_literal)))
{
if !tracked_pred.as_ref().map_or(false, |t| {
t.current_literals.as_ref().map_or(false, |tt| {
tt.iter().any(|literal| *literal == *required_literal)
})
}) {
return false;
}
}
@ -365,12 +382,17 @@ pub fn update_subject_validity(
.any(|dt| dt.valType == OrmSchemaLiteralType::shape)
{
// If we have a nested shape, we need to check if the nested objects are tracked and valid.
let tracked_children = tracked_pred.as_ref().map(|tp|
tp.tracked_children.iter().filter_map(|weak_tc| weak_tc.upgrade()).collect::<Vec<_>>()
);
let tracked_children = tracked_pred.as_ref().map(|tp| {
tp.tracked_children
.iter()
.filter_map(|weak_tc| weak_tc.upgrade())
.collect::<Vec<_>>()
});
// First, Count valid, invalid, unknowns, and untracked
let counts = tracked_children.as_ref()
.map_or((0,0,0,0),|children| children.iter().map(|tc| {
let counts = tracked_children.as_ref().map_or((0, 0, 0, 0), |children| {
children
.iter()
.map(|tc| {
if tc.valid == OrmTrackedSubjectValidity::Valid {
(1, 0, 0, 0)
} else if tc.valid == OrmTrackedSubjectValidity::Invalid {
@ -385,7 +407,8 @@ pub fn update_subject_validity(
})
.fold((0, 0, 0, 0), |(v1, i1, u1, ut1), o| {
(v1 + o.0, i1 + o.1, u1 + o.2, ut1 + o.3)
}));
})
});
if counts.1 > 0 && p_schema.extra != Some(true) {
// If we have at least one invalid nested object and no extra allowed, invalid.
@ -401,7 +424,7 @@ pub fn update_subject_validity(
// After that we need to reevaluate this (subject,shape) again.
need_evaluation.push((s_change.subject_iri.to_string(), shape.iri.clone(), false));
// Also schedule untracked children for fetching and validation.
tracked_children.as_ref().map(|children|
tracked_children.as_ref().map(|children| {
for child in children {
if child.valid == OrmTrackedSubjectValidity::Untracked {
need_evaluation.push((
@ -411,12 +434,12 @@ pub fn update_subject_validity(
));
}
}
);
});
} else if counts.2 > 0 {
// If we have unknown nested objects, we need to wait for their evaluation.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Pending);
// Schedule unknown children (NotEvaluated) for re-evaluation without fetch.
tracked_children.as_ref().map(|children|
tracked_children.as_ref().map(|children| {
for child in children {
if child.valid == OrmTrackedSubjectValidity::Pending {
need_evaluation.push((
@ -426,7 +449,7 @@ pub fn update_subject_validity(
));
}
}
);
});
} else {
// All nested objects are valid and cardinality is correct.
// We are valid with this predicate.
@ -487,7 +510,11 @@ pub fn update_subject_validity(
return tracked_subject
.parents
.values()
.filter_map(|parent| parent.upgrade().map(|parent| {(parent.subject_iri.clone(), parent.shape.iri.clone(), false)}) )
.filter_map(|parent| {
parent
.upgrade()
.map(|parent| (parent.subject_iri.clone(), parent.shape.iri.clone(), false))
})
// Add `need_evaluation`.
.chain(need_evaluation)
.collect();

@ -112,7 +112,7 @@ pub struct Verifier {
in_memory_outbox: Vec<EventOutboxStorage>,
uploads: BTreeMap<u32, RandomAccessFile>,
branch_subscriptions: HashMap<BranchId, Sender<AppResponse>>,
pub(crate) orm_subscriptions: HashMap<NuriV0, OrmSubscription>,
pub(crate) orm_subscriptions: HashMap<NuriV0, Vec<OrmSubscription>>,
pub(crate) temporary_repo_certificates: HashMap<RepoId, ObjectRef>,
}

Loading…
Cancel
Save