@@ -565,6 +565,36 @@ export class DbService {
565565 { id, ontology : meta . ontology , acl, eName } ,
566566 ) ;
567567
568+ // Deduplicate envelopes — if multiple Envelope nodes share the
569+ // same ontology (field name), keep the first and delete the rest.
570+ // This prevents non-deterministic reads where collect(e) returns
571+ // duplicates in undefined order and reduce picks the wrong one.
572+ const seen = new Map < string , string > ( ) ; // ontology → kept envelope id
573+ const dupsToDelete : string [ ] = [ ] ;
574+ for ( const env of existing . envelopes ) {
575+ if ( seen . has ( env . ontology ) ) {
576+ dupsToDelete . push ( env . id ) ;
577+ } else {
578+ seen . set ( env . ontology , env . id ) ;
579+ }
580+ }
581+ if ( dupsToDelete . length > 0 ) {
582+ console . warn (
583+ `[eVault] Cleaning ${ dupsToDelete . length } duplicate envelope(s) for MetaEnvelope ${ id } ` ,
584+ ) ;
585+ for ( const dupId of dupsToDelete ) {
586+ await this . runQueryInternal (
587+ `MATCH (e:Envelope { id: $envelopeId }) DETACH DELETE e` ,
588+ { envelopeId : dupId } ,
589+ ) ;
590+ }
591+ // Remove deleted dupes from the existing list so the update
592+ // loop below doesn't try to reference them.
593+ existing . envelopes = existing . envelopes . filter (
594+ ( e ) => ! dupsToDelete . includes ( e . id ) ,
595+ ) ;
596+ }
597+
568598 const createdEnvelopes : Envelope < T [ keyof T ] > [ ] = [ ] ;
569599 let counter = 0 ;
570600
@@ -601,21 +631,18 @@ export class DbService {
601631 valueType,
602632 } ) ;
603633 } else {
604- // Create new envelope
634+ // Create new envelope — use MERGE on the relationship
635+ // + ontology to prevent duplicate Envelopes if two
636+ // concurrent updates race.
605637 const envW3id = await new W3IDBuilder ( ) . build ( ) ;
606638 const envelopeId = envW3id . id ;
607639
608640 await this . runQueryInternal (
609641 `
610642 MATCH (m:MetaEnvelope { id: $metaId, eName: $eName })
611- CREATE (${ alias } :Envelope {
612- id: $${ alias } _id,
613- ontology: $${ alias } _ontology,
614- value: $${ alias } _value,
615- valueType: $${ alias } _type
616- })
617- WITH m, ${ alias }
618- MERGE (m)-[:LINKS_TO]->(${ alias } )
643+ MERGE (m)-[:LINKS_TO]->(${ alias } :Envelope { ontology: $${ alias } _ontology })
644+ ON CREATE SET ${ alias } .id = $${ alias } _id, ${ alias } .value = $${ alias } _value, ${ alias } .valueType = $${ alias } _type
645+ ON MATCH SET ${ alias } .value = $${ alias } _value, ${ alias } .valueType = $${ alias } _type
619646 ` ,
620647 {
621648 metaId : id ,
0 commit comments