Skip to main content

oxirs_core/jsonld/
streaming.rs

1//! Ultra-high performance streaming JSON-LD processing
2//!
3//! This module provides advanced streaming capabilities for JSON-LD processing
4//! with zero-copy operations, SIMD acceleration, and adaptive buffering.
5
6use crate::{
7    jsonld::JsonLdParseError,
8    model::{NamedNode, Object, Predicate, Quad, Subject, Triple},
9    optimization::{SimdJsonProcessor, TermInterner, TermInternerExt, ZeroCopyBuffer},
10};
11
12use super::context::{JsonLdLoadDocumentOptions, JsonLdRemoteDocument};
13use super::profile::JsonLdProfileSet;
14// Removed unused async_trait::async_trait import
15use dashmap::DashMap;
16// Removed unused futures::{SinkExt, StreamExt} imports
17use parking_lot::Mutex;
18#[cfg(feature = "parallel")]
19use rayon::prelude::*;
20use serde_json::{Map, Value};
21use std::{
22    collections::VecDeque,
23    error::Error as StdError,
24    sync::{
25        atomic::{AtomicUsize, Ordering},
26        Arc,
27    },
28};
29use tokio::{
30    io::{AsyncRead, AsyncReadExt, BufReader},
31    sync::{mpsc, RwLock, Semaphore},
32    time::{Duration, Instant},
33};
34
35/// Callback used to resolve remote JSON-LD `@context` documents referenced by
36/// IRI (e.g. `"@context": "https://schema.org/"`).
37///
38/// It mirrors the loader mechanism used by
39/// [`JsonLdContextProcessor`](super::context::JsonLdContextProcessor): given a
40/// context IRI and [`JsonLdLoadDocumentOptions`] it must return the fetched
41/// document bytes (or an error). The streaming parser never performs network
42/// I/O itself — a resolver must be supplied explicitly via
43/// [`UltraStreamingJsonLdParser::with_document_loader`]; otherwise any document
44/// that references a remote context fails loudly rather than silently dropping
45/// the import.
46pub type StreamingDocumentLoader = dyn Fn(
47        &str,
48        &JsonLdLoadDocumentOptions,
49    ) -> Result<JsonLdRemoteDocument, Box<dyn StdError + Send + Sync>>
50    + Send
51    + Sync;
52
53/// Ultra-high performance streaming JSON-LD parser with adaptive optimizations
54pub struct UltraStreamingJsonLdParser {
55    config: StreamingConfig,
56    context_cache: Arc<DashMap<String, Arc<Value>>>,
57    term_interner: Arc<TermInterner>,
58    performance_monitor: Arc<PerformanceMonitor>,
59    simd_processor: SimdJsonProcessor,
60    buffer_pool: Arc<BufferPool>,
61    document_loader: Option<Arc<StreamingDocumentLoader>>,
62}
63
64/// Advanced configuration for streaming JSON-LD processing
65#[derive(Debug, Clone)]
66pub struct StreamingConfig {
67    /// Chunk size for reading data (adaptive)
68    pub chunk_size: usize,
69    /// Maximum number of concurrent processing threads
70    pub max_concurrent_threads: usize,
71    /// Buffer size for intermediate processing
72    pub buffer_size: usize,
73    /// Enable SIMD acceleration for JSON parsing
74    pub enable_simd: bool,
75    /// Context caching configuration
76    pub context_cache_size: usize,
77    /// Adaptive buffering threshold
78    pub adaptive_threshold: f64,
79    /// Memory pressure detection
80    pub memory_pressure_threshold: usize,
81    /// Zero-copy optimization level
82    pub zero_copy_level: ZeroCopyLevel,
83    /// Performance profiling enabled
84    pub enable_profiling: bool,
85}
86
87/// Zero-copy optimization levels
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum ZeroCopyLevel {
90    /// No zero-copy optimizations
91    None,
92    /// Basic zero-copy for string references
93    Basic,
94    /// Advanced zero-copy with arena allocation
95    Advanced,
96    /// Maximum zero-copy with custom allocators
97    Maximum,
98}
99
100/// Real-time performance monitoring for streaming operations
101pub struct PerformanceMonitor {
102    total_bytes_processed: AtomicUsize,
103    total_triples_parsed: AtomicUsize,
104    parse_errors: AtomicUsize,
105    context_cache_hits: AtomicUsize,
106    context_cache_misses: AtomicUsize,
107    simd_operations: AtomicUsize,
108    zero_copy_operations: AtomicUsize,
109    start_time: Instant,
110    chunk_processing_times: Arc<Mutex<VecDeque<Duration>>>,
111}
112
113/// Adaptive buffer pool for high-throughput processing
114pub struct BufferPool {
115    available_buffers: Arc<Mutex<Vec<ZeroCopyBuffer>>>,
116    buffer_size: usize,
117    max_buffers: usize,
118    current_buffers: AtomicUsize,
119}
120
121/// High-performance streaming sink for processed triples
122#[async_trait::async_trait]
123pub trait StreamingSink: Send + Sync {
124    type Error: Send + Sync + std::error::Error + 'static;
125
126    async fn process_triple_batch(&mut self, triples: Vec<Triple>) -> Result<(), Self::Error>;
127    async fn process_quad_batch(&mut self, quads: Vec<Quad>) -> Result<(), Self::Error>;
128    async fn flush(&mut self) -> Result<(), Self::Error>;
129    fn performance_statistics(&self) -> SinkStatistics;
130}
131
132/// Statistics for sink performance monitoring
133#[derive(Debug, Clone)]
134pub struct SinkStatistics {
135    pub total_triples_processed: usize,
136    pub total_quads_processed: usize,
137    pub average_batch_size: f64,
138    pub processing_rate_per_second: f64,
139    pub memory_usage_bytes: usize,
140}
141
142impl Default for StreamingConfig {
143    fn default() -> Self {
144        Self {
145            chunk_size: 64 * 1024, // 64KB adaptive starting point
146            max_concurrent_threads: std::thread::available_parallelism()
147                .map(|n| n.get())
148                .unwrap_or(1)
149                * 2,
150            buffer_size: 1024 * 1024, // 1MB buffer
151            enable_simd: true,
152            context_cache_size: 10000,
153            adaptive_threshold: 0.8,
154            memory_pressure_threshold: 8 * 1024 * 1024 * 1024, // 8GB
155            zero_copy_level: ZeroCopyLevel::Advanced,
156            enable_profiling: true,
157        }
158    }
159}
160
161impl UltraStreamingJsonLdParser {
162    /// Create a new ultra-performance streaming parser
163    pub fn new(config: StreamingConfig) -> Self {
164        Self {
165            context_cache: Arc::new(DashMap::with_capacity(config.context_cache_size)),
166            term_interner: Arc::new(TermInterner::new()),
167            performance_monitor: Arc::new(PerformanceMonitor::new()),
168            simd_processor: SimdJsonProcessor::new(),
169            buffer_pool: Arc::new(BufferPool::new(config.buffer_size, 100)),
170            document_loader: None,
171            config,
172        }
173    }
174
175    /// Attach a resolver for remote `@context` documents referenced by IRI.
176    ///
177    /// Without a loader, encountering a string `@context` (a remote context
178    /// reference) during streaming produces an explicit
179    /// [`JsonLdParseError`] instead of silently substituting an empty context.
180    pub fn with_document_loader(mut self, loader: Arc<StreamingDocumentLoader>) -> Self {
181        self.document_loader = Some(loader);
182        self
183    }
184
185    /// Stream parse JSON-LD with ultra-high performance optimizations
186    pub async fn stream_parse<R, S>(
187        &mut self,
188        reader: R,
189        mut sink: S,
190    ) -> Result<StreamingStatistics, JsonLdParseError>
191    where
192        R: AsyncRead + Unpin + Send + 'static,
193        S: StreamingSink + Send + 'static,
194        S::Error: 'static,
195    {
196        let mut buf_reader = BufReader::with_capacity(self.config.chunk_size, reader);
197        let (tx, mut rx) = mpsc::channel::<ProcessingChunk>(self.config.buffer_size);
198        let (triple_tx, mut triple_rx) = mpsc::channel::<Vec<Triple>>(100);
199        let semaphore = Arc::new(Semaphore::new(self.config.max_concurrent_threads));
200
201        // Spawn sink processing task
202        let sink_handle = tokio::spawn(async move {
203            while let Some(batch) = triple_rx.recv().await {
204                sink.process_triple_batch(batch)
205                    .await
206                    .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))?;
207            }
208
209            sink.flush()
210                .await
211                .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))?;
212
213            Ok::<(), JsonLdParseError>(())
214        });
215
216        // Spawn parallel processing tasks
217        let processing_handle = tokio::spawn({
218            let config = self.config.clone();
219            let context_cache = Arc::clone(&self.context_cache);
220            let term_interner = Arc::clone(&self.term_interner);
221            let performance_monitor = Arc::clone(&self.performance_monitor);
222            let simd_processor = self.simd_processor.clone();
223            let document_loader = self.document_loader.clone();
224            let triple_tx = triple_tx.clone();
225
226            async move {
227                let mut batch_buffer = Vec::with_capacity(config.buffer_size);
228                // The raw byte stream is split into fixed-size transport chunks
229                // whose boundaries fall at arbitrary offsets (mid-object,
230                // mid-string, ...). We therefore reassemble the stream and only
231                // hand *complete* top-level JSON values to the JSON parser,
232                // maintaining tokenizer state across chunk boundaries.
233                let mut splitter = TopLevelJsonSplitter::new();
234                let mut eof = false;
235
236                while !eof {
237                    match rx.recv().await {
238                        Some(chunk) => {
239                            let _permit = semaphore.acquire().await.map_err(|_| {
240                                JsonLdParseError::ProcessingError(
241                                    "processing semaphore closed unexpectedly".to_string(),
242                                )
243                            })?;
244                            splitter.push(&chunk.data);
245                        }
246                        None => {
247                            // Reader side finished: allow the splitter to emit a
248                            // final EOF-terminated primitive value if any.
249                            eof = true;
250                            splitter.mark_eof();
251                        }
252                    }
253
254                    while let Some(document) = splitter.next_complete_value()? {
255                        // Process a complete JSON-LD document/value with SIMD
256                        // acceleration if available.
257                        let processed_triples = if config.enable_simd {
258                            Self::process_chunk_simd(
259                                &document,
260                                &context_cache,
261                                &term_interner,
262                                &simd_processor,
263                                &document_loader,
264                            )
265                            .await?
266                        } else {
267                            Self::process_chunk_standard(
268                                &document,
269                                &context_cache,
270                                &term_interner,
271                                &document_loader,
272                            )
273                            .await?
274                        };
275
276                        performance_monitor.record_triples_parsed(processed_triples.len());
277
278                        batch_buffer.extend(processed_triples);
279
280                        // Adaptive batching based on performance metrics
281                        if batch_buffer.len() >= config.buffer_size
282                            || performance_monitor.should_flush_batch()
283                        {
284                            triple_tx
285                                .send(std::mem::take(&mut batch_buffer))
286                                .await
287                                .map_err(|_| {
288                                    JsonLdParseError::ProcessingError(
289                                        "Triple channel send failed".to_string(),
290                                    )
291                                })?;
292                        }
293                    }
294                }
295
296                // The stream has ended: reject any trailing, truncated value
297                // (e.g. a document cut off mid-object) rather than silently
298                // discarding it.
299                splitter.finish()?;
300
301                // Flush remaining triples
302                if !batch_buffer.is_empty() {
303                    triple_tx.send(batch_buffer).await.map_err(|_| {
304                        JsonLdParseError::ProcessingError("Triple channel send failed".to_string())
305                    })?;
306                }
307
308                Ok::<(), JsonLdParseError>(())
309            }
310        });
311
312        // Read and chunk data adaptively
313        let mut buffer = self.buffer_pool.get_buffer().await;
314        let mut total_bytes = 0;
315
316        loop {
317            match buf_reader.read(buffer.as_mut_slice()).await {
318                Ok(0) => break, // EOF
319                Ok(n) => {
320                    buffer.set_len(n);
321                    total_bytes += n;
322                    self.performance_monitor.record_bytes_processed(n);
323
324                    // Adaptive chunk size adjustment
325                    if self.should_adjust_chunk_size(n) {
326                        self.adjust_chunk_size_adaptive().await;
327                    }
328
329                    let chunk = ProcessingChunk {
330                        data: buffer.as_slice().to_vec(),
331                        timestamp: Instant::now(),
332                        sequence_id: total_bytes,
333                    };
334
335                    tx.send(chunk).await.map_err(|_| {
336                        JsonLdParseError::ProcessingError("Channel send failed".to_string())
337                    })?;
338
339                    buffer = self.buffer_pool.get_buffer().await;
340                }
341                Err(e) => return Err(JsonLdParseError::Io(e)),
342            }
343        }
344
345        drop(tx); // Signal completion to processing task
346        processing_handle
347            .await
348            .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))??;
349
350        drop(triple_tx); // Signal completion to sink task
351        sink_handle
352            .await
353            .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))??;
354
355        Ok(self.performance_monitor.get_statistics())
356    }
357
358    /// Process a complete JSON-LD document with SIMD acceleration
359    async fn process_chunk_simd(
360        document: &[u8],
361        context_cache: &DashMap<String, Arc<Value>>,
362        term_interner: &TermInterner,
363        simd_processor: &SimdJsonProcessor,
364        document_loader: &Option<Arc<StreamingDocumentLoader>>,
365    ) -> Result<Vec<Triple>, JsonLdParseError> {
366        let start = Instant::now();
367
368        // SIMD-accelerated JSON parsing (document is guaranteed to be a
369        // complete, self-contained top-level JSON value by the splitter).
370        let json_value = simd_processor
371            .parse_json(document)
372            .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))?;
373
374        // Zero-copy context resolution
375        let context =
376            Self::resolve_context_zero_copy(&json_value, context_cache, document_loader).await?;
377
378        // Parallel triple extraction with work-stealing
379        #[cfg(feature = "parallel")]
380        let triples = Self::extract_triples_parallel(&json_value, &context, term_interner).await?;
381        #[cfg(not(feature = "parallel"))]
382        let triples = Self::extract_triples_standard(&json_value, &context, term_interner).await?;
383
384        // Record performance metrics
385        let _processing_time = start.elapsed();
386        // performance_monitor.record_chunk_processing_time(processing_time);
387
388        Ok(triples)
389    }
390
391    /// Process a complete JSON-LD document with standard methods
392    async fn process_chunk_standard(
393        document: &[u8],
394        context_cache: &DashMap<String, Arc<Value>>,
395        term_interner: &TermInterner,
396        document_loader: &Option<Arc<StreamingDocumentLoader>>,
397    ) -> Result<Vec<Triple>, JsonLdParseError> {
398        // Standard JSON parsing (document is a complete top-level JSON value).
399        let json_value: Value = serde_json::from_slice(document)
400            .map_err(|e| JsonLdParseError::ProcessingError(e.to_string()))?;
401
402        // Context resolution with caching
403        let context =
404            Self::resolve_context_cached(&json_value, context_cache, document_loader).await?;
405
406        // Triple extraction
407        let triples = Self::extract_triples_standard(&json_value, &context, term_interner).await?;
408
409        Ok(triples)
410    }
411
412    /// Zero-copy context resolution
413    async fn resolve_context_zero_copy(
414        json_value: &Value,
415        context_cache: &DashMap<String, Arc<Value>>,
416        document_loader: &Option<Arc<StreamingDocumentLoader>>,
417    ) -> Result<Arc<Value>, JsonLdParseError> {
418        if let Some(context_ref) = json_value.get("@context") {
419            if let Some(context_str) = context_ref.as_str() {
420                if let Some(cached_context) = context_cache.get(context_str) {
421                    return Ok(Arc::clone(&cached_context));
422                }
423
424                // Resolve and cache context
425                let resolved_context =
426                    Self::resolve_remote_context(context_str, document_loader).await?;
427                let context_arc = Arc::new(resolved_context);
428                context_cache.insert(context_str.to_string(), Arc::clone(&context_arc));
429                return Ok(context_arc);
430            }
431
432            // Inline context object (or array of contexts): use it verbatim as
433            // the active context for term expansion.
434            if context_ref.is_object() || context_ref.is_array() {
435                return Ok(Arc::new(context_ref.clone()));
436            }
437        }
438
439        // Default context
440        Ok(Arc::new(Value::Object(Map::new())))
441    }
442
443    /// Cached context resolution
444    async fn resolve_context_cached(
445        json_value: &Value,
446        context_cache: &DashMap<String, Arc<Value>>,
447        document_loader: &Option<Arc<StreamingDocumentLoader>>,
448    ) -> Result<Arc<Value>, JsonLdParseError> {
449        // Similar to zero-copy but with different optimization strategy
450        Self::resolve_context_zero_copy(json_value, context_cache, document_loader).await
451    }
452
453    /// Parallel triple extraction with work-stealing
454    #[cfg(feature = "parallel")]
455    async fn extract_triples_parallel(
456        json_value: &Value,
457        context: &Value,
458        term_interner: &TermInterner,
459    ) -> Result<Vec<Triple>, JsonLdParseError> {
460        if let Value::Array(objects) = json_value {
461            // Parallel processing of JSON-LD objects
462            let triples: Result<Vec<Vec<Triple>>, JsonLdParseError> = objects
463                .par_iter()
464                .map(|obj| Self::extract_triples_from_object(obj, context, term_interner))
465                .collect();
466
467            Ok(triples?.into_iter().flatten().collect())
468        } else {
469            Self::extract_triples_from_object(json_value, context, term_interner)
470        }
471    }
472
473    /// Standard triple extraction
474    async fn extract_triples_standard(
475        json_value: &Value,
476        context: &Value,
477        term_interner: &TermInterner,
478    ) -> Result<Vec<Triple>, JsonLdParseError> {
479        // A top-level JSON-LD document may be a single node object or an array
480        // of node objects; handle both.
481        if let Value::Array(objects) = json_value {
482            let mut triples = Vec::new();
483            for obj in objects {
484                triples.extend(Self::extract_triples_from_object(
485                    obj,
486                    context,
487                    term_interner,
488                )?);
489            }
490            Ok(triples)
491        } else {
492            Self::extract_triples_from_object(json_value, context, term_interner)
493        }
494    }
495
496    /// Extract triples from a single JSON-LD object
497    fn extract_triples_from_object(
498        obj: &Value,
499        context: &Value,
500        term_interner: &TermInterner,
501    ) -> Result<Vec<Triple>, JsonLdParseError> {
502        let mut triples = Vec::new();
503
504        if let Value::Object(map) = obj {
505            // Extract subject
506            let subject: Subject = if let Some(id) = map.get("@id") {
507                Subject::NamedNode(term_interner.intern_named_node(id.as_str().ok_or_else(
508                    || JsonLdParseError::ProcessingError("Invalid @id".to_string()),
509                )?)?)
510            } else {
511                // Generate blank node
512                Subject::BlankNode(term_interner.intern_blank_node())
513            };
514
515            // Process properties
516            for (key, value) in map {
517                if key.starts_with('@') {
518                    continue; // Skip JSON-LD keywords
519                }
520
521                // Expand property IRI using context
522                let predicate_iri = Self::expand_property(key, context)?;
523                let predicate = term_interner.intern_named_node(&predicate_iri)?;
524
525                // Process values
526                match value {
527                    Value::Array(values) => {
528                        for val in values {
529                            if let Some(triple) = Self::create_triple_from_value(
530                                subject.clone(),
531                                predicate.clone(),
532                                val,
533                                context,
534                                term_interner,
535                            )? {
536                                triples.push(triple);
537                            }
538                        }
539                    }
540                    _ => {
541                        if let Some(triple) = Self::create_triple_from_value(
542                            subject.clone(),
543                            predicate,
544                            value,
545                            context,
546                            term_interner,
547                        )? {
548                            triples.push(triple);
549                        }
550                    }
551                }
552            }
553        }
554
555        Ok(triples)
556    }
557
558    /// Create triple from JSON-LD value
559    fn create_triple_from_value(
560        subject: Subject,
561        predicate: NamedNode,
562        value: &Value,
563        _context: &Value,
564        term_interner: &TermInterner,
565    ) -> Result<Option<Triple>, JsonLdParseError> {
566        let object: Object = match value {
567            Value::String(s) => {
568                // Check if it's an IRI or literal
569                if s.starts_with("http://") || s.starts_with("https://") {
570                    Object::NamedNode(term_interner.intern_named_node(s)?)
571                } else {
572                    Object::Literal(term_interner.intern_literal(s)?)
573                }
574            }
575            Value::Object(obj) => {
576                if let Some(id) = obj.get("@id") {
577                    // Object reference
578                    Object::NamedNode(term_interner.intern_named_node(id.as_str().ok_or_else(
579                        || JsonLdParseError::ProcessingError("Invalid @id in object".to_string()),
580                    )?)?)
581                } else if let Some(val) = obj.get("@value") {
582                    // Typed literal
583                    let literal_value = val.as_str().ok_or_else(|| {
584                        JsonLdParseError::ProcessingError("Invalid @value".to_string())
585                    })?;
586
587                    if let Some(datatype) = obj.get("@type") {
588                        let datatype_iri = datatype.as_str().ok_or_else(|| {
589                            JsonLdParseError::ProcessingError("Invalid @type".to_string())
590                        })?;
591                        Object::Literal(
592                            term_interner
593                                .intern_literal_with_datatype(literal_value, datatype_iri)?,
594                        )
595                    } else if let Some(lang) = obj.get("@language") {
596                        let language = lang.as_str().ok_or_else(|| {
597                            JsonLdParseError::ProcessingError("Invalid @language".to_string())
598                        })?;
599                        Object::Literal(
600                            term_interner.intern_literal_with_language(literal_value, language)?,
601                        )
602                    } else {
603                        Object::Literal(term_interner.intern_literal(literal_value)?)
604                    }
605                } else {
606                    return Ok(None); // Skip complex nested objects for now
607                }
608            }
609            Value::Number(n) => Object::Literal(term_interner.intern_literal(&n.to_string())?),
610            Value::Bool(b) => Object::Literal(term_interner.intern_literal(&b.to_string())?),
611            _ => return Ok(None),
612        };
613
614        Ok(Some(Triple::new(
615            subject,
616            Predicate::NamedNode(predicate),
617            object,
618        )))
619    }
620
621    /// Expand a JSON-LD term (a property key) to an absolute IRI using the
622    /// active context.
623    ///
624    /// This applies the relevant parts of the JSON-LD 1.1 IRI expansion
625    /// algorithm for property keys:
626    ///
627    /// 1. an explicit term definition in the context (a string mapping, or an
628    ///    object with `@id`), recursively resolving compact-IRI mappings;
629    /// 2. a compact IRI `prefix:suffix` whose `prefix` is defined in the
630    ///    context;
631    /// 3. an already-absolute IRI (contains a `scheme:` / `://`);
632    /// 4. `@vocab`-based expansion for plain terms.
633    ///
634    /// Under strict processing (the streaming parser's only mode) a term that
635    /// matches none of these **fails loudly** with a [`JsonLdParseError`]
636    /// rather than being silently mapped to a fabricated placeholder namespace.
637    fn expand_property(property: &str, context: &Value) -> Result<String, JsonLdParseError> {
638        Self::expand_term(property, context, 0)
639    }
640
641    fn expand_term(term: &str, context: &Value, depth: usize) -> Result<String, JsonLdParseError> {
642        if depth > 16 {
643            return Err(JsonLdParseError::ProcessingError(format!(
644                "cyclic JSON-LD term definition while expanding '{term}'"
645            )));
646        }
647
648        // 1. Explicit term definition in the active context.
649        if let Some(def) = Self::context_lookup(context, term) {
650            if let Some(mapping) = Self::term_definition_iri(def) {
651                if mapping != term {
652                    // The mapping itself may be a compact IRI or another term.
653                    return Self::expand_iri(&mapping, context, depth + 1);
654                }
655            }
656        }
657
658        Self::expand_iri(term, context, depth)
659    }
660
661    /// Expand an IRI-ish string: a compact IRI, an absolute IRI, or (for a bare
662    /// term) via `@vocab`.
663    fn expand_iri(value: &str, context: &Value, depth: usize) -> Result<String, JsonLdParseError> {
664        if depth > 16 {
665            return Err(JsonLdParseError::ProcessingError(format!(
666                "cyclic JSON-LD prefix/term definition while expanding '{value}'"
667            )));
668        }
669        if let Some((prefix, suffix)) = value.split_once(':') {
670            // Blank node identifiers and scheme-relative / absolute IRIs are
671            // used verbatim.
672            if prefix.is_empty() || prefix == "_" || suffix.starts_with("//") {
673                return Ok(value.to_string());
674            }
675
676            // Compact IRI whose prefix is defined in the context.
677            if let Some(def) = Self::context_lookup(context, prefix) {
678                if let Some(prefix_iri) = Self::term_definition_iri(def) {
679                    if prefix_iri != prefix {
680                        let base = Self::expand_iri(&prefix_iri, context, depth + 1)?;
681                        return Ok(format!("{base}{suffix}"));
682                    }
683                }
684            }
685
686            // A `scheme:...` form with an unknown prefix is treated as an
687            // already-absolute IRI.
688            return Ok(value.to_string());
689        }
690
691        // No colon: a bare term. Try `@vocab`.
692        if let Some(vocab) = Self::context_vocab(context) {
693            return Ok(format!("{vocab}{value}"));
694        }
695
696        Err(JsonLdParseError::ProcessingError(format!(
697            "cannot expand JSON-LD term '{value}' to an absolute IRI: no matching term \
698             definition, prefix, or @vocab in the active context"
699        )))
700    }
701
702    /// Look up a key in a JSON-LD context, which may be a single object or an
703    /// array of context objects (later entries take precedence).
704    fn context_lookup<'a>(context: &'a Value, key: &str) -> Option<&'a Value> {
705        match context {
706            Value::Object(ctx) => ctx.get(key),
707            Value::Array(contexts) => contexts
708                .iter()
709                .rev()
710                .find_map(|c| Self::context_lookup(c, key)),
711            _ => None,
712        }
713    }
714
715    /// Extract the `@vocab` mapping from a context (object or array).
716    fn context_vocab(context: &Value) -> Option<String> {
717        Self::context_lookup(context, "@vocab")
718            .and_then(|v| v.as_str())
719            .map(|s| s.to_string())
720    }
721
722    /// Resolve a term definition to its raw IRI mapping. A term definition is
723    /// either a string (the IRI) or an object carrying an `@id`.
724    fn term_definition_iri(def: &Value) -> Option<String> {
725        match def {
726            Value::String(s) => Some(s.clone()),
727            Value::Object(o) => o.get("@id").and_then(|v| v.as_str()).map(|s| s.to_string()),
728            _ => None,
729        }
730    }
731
732    /// Resolve a remote `@context` referenced by IRI.
733    ///
734    /// The streaming parser performs no network I/O of its own. Resolution is
735    /// delegated to the configured [`StreamingDocumentLoader`]; if none is
736    /// configured, this **fails loudly** rather than silently substituting an
737    /// empty context (which would corrupt every term in the document).
738    async fn resolve_remote_context(
739        context_iri: &str,
740        document_loader: &Option<Arc<StreamingDocumentLoader>>,
741    ) -> Result<Value, JsonLdParseError> {
742        let loader = document_loader.as_ref().ok_or_else(|| {
743            JsonLdParseError::ProcessingError(format!(
744                "cannot resolve remote JSON-LD context '{context_iri}': no document loader \
745                 configured. Supply one via UltraStreamingJsonLdParser::with_document_loader"
746            ))
747        })?;
748
749        let options = JsonLdLoadDocumentOptions {
750            request_profile: JsonLdProfileSet::from(super::profile::JsonLdProfile::Context),
751        };
752
753        let remote = loader(context_iri, &options).map_err(|e| {
754            JsonLdParseError::ProcessingError(format!(
755                "failed to load remote JSON-LD context '{context_iri}': {e}"
756            ))
757        })?;
758
759        let parsed: Value = serde_json::from_slice(&remote.document).map_err(|e| {
760            JsonLdParseError::ProcessingError(format!(
761                "remote JSON-LD context '{context_iri}' is not valid JSON: {e}"
762            ))
763        })?;
764
765        // A context document is conventionally `{"@context": {...}}`; unwrap the
766        // inner context so it can be used directly for term expansion.
767        let context = match parsed {
768            Value::Object(mut obj) => match obj.remove("@context") {
769                Some(inner) => inner,
770                None => Value::Object(obj),
771            },
772            other => other,
773        };
774
775        Ok(context)
776    }
777
778    /// Check if chunk size should be adjusted
779    fn should_adjust_chunk_size(&self, bytes_read: usize) -> bool {
780        let target_size = self.config.chunk_size;
781        let threshold = (target_size as f64 * self.config.adaptive_threshold) as usize;
782        bytes_read < threshold || bytes_read > target_size * 2
783    }
784
785    /// Adaptively adjust chunk size based on performance
786    async fn adjust_chunk_size_adaptive(&mut self) {
787        let avg_processing_time = self.performance_monitor.average_chunk_processing_time();
788        let memory_pressure = self.performance_monitor.memory_pressure_detected();
789
790        if memory_pressure {
791            self.config.chunk_size = (self.config.chunk_size / 2).max(1024);
792        } else if avg_processing_time < Duration::from_millis(10) {
793            self.config.chunk_size = (self.config.chunk_size * 2).min(1024 * 1024);
794        }
795    }
796}
797
798/// Chunk of data being processed
799#[derive(Debug)]
800struct ProcessingChunk {
801    data: Vec<u8>,
802    #[allow(dead_code)]
803    timestamp: Instant,
804    #[allow(dead_code)]
805    sequence_id: usize,
806}
807
808/// Incremental scanning state for [`TopLevelJsonSplitter`].
809#[derive(Debug, Clone, Copy)]
810enum SplitState {
811    /// Between values, skipping insignificant whitespace.
812    Idle,
813    /// Inside a `{...}` / `[...]` container that began at `start`.
814    Container {
815        start: usize,
816        depth: usize,
817        in_string: bool,
818        escaped: bool,
819    },
820    /// Inside a `"..."` string scalar that began at `start`.
821    StringScalar { start: usize, escaped: bool },
822    /// Inside a bare primitive (number / `true` / `false` / `null`) that began
823    /// at `start`.
824    Primitive { start: usize },
825}
826
827/// Reassembles a byte stream that has been split into arbitrary-boundary chunks
828/// and yields **complete** top-level JSON values.
829///
830/// A "streaming" JSON-LD parser cannot simply hand each transport chunk to
831/// `serde_json`, because chunk boundaries fall at arbitrary offsets — in the
832/// middle of an object, string, escape sequence, etc. This splitter maintains a
833/// small tokenizer state (container depth, in-string / escape flags) across
834/// chunk boundaries and only emits a byte slice once it forms a whole,
835/// self-contained top-level JSON value. It supports a single large value
836/// spanning many chunks as well as several concatenated / newline-delimited
837/// values in one stream.
838#[derive(Debug)]
839struct TopLevelJsonSplitter {
840    buf: Vec<u8>,
841    /// Scan cursor into `buf`.
842    pos: usize,
843    /// Set once the underlying reader has reached EOF.
844    eof: bool,
845    state: SplitState,
846}
847
848impl TopLevelJsonSplitter {
849    fn new() -> Self {
850        Self {
851            buf: Vec::new(),
852            pos: 0,
853            eof: false,
854            state: SplitState::Idle,
855        }
856    }
857
858    #[inline]
859    fn is_ws(byte: u8) -> bool {
860        matches!(byte, b' ' | b'\t' | b'\n' | b'\r')
861    }
862
863    /// Append freshly read bytes to the reassembly buffer.
864    fn push(&mut self, data: &[u8]) {
865        self.buf.extend_from_slice(data);
866    }
867
868    /// Signal that no further bytes will arrive.
869    fn mark_eof(&mut self) {
870        self.eof = true;
871    }
872
873    /// Extract the next complete top-level JSON value, if one is fully
874    /// available. Returns `Ok(None)` when more input is required (or the stream
875    /// is exhausted with only whitespace remaining).
876    fn next_complete_value(&mut self) -> Result<Option<Vec<u8>>, JsonLdParseError> {
877        loop {
878            match self.state {
879                SplitState::Idle => {
880                    // Drop already-consumed bytes to keep memory bounded when a
881                    // stream carries many concatenated values.
882                    if self.pos > 0 {
883                        self.buf.drain(0..self.pos);
884                        self.pos = 0;
885                    }
886                    while self.pos < self.buf.len() && Self::is_ws(self.buf[self.pos]) {
887                        self.pos += 1;
888                    }
889                    if self.pos >= self.buf.len() {
890                        self.buf.drain(0..self.pos);
891                        self.pos = 0;
892                        return Ok(None);
893                    }
894                    let start = self.pos;
895                    match self.buf[self.pos] {
896                        b'{' | b'[' => {
897                            self.pos += 1;
898                            self.state = SplitState::Container {
899                                start,
900                                depth: 1,
901                                in_string: false,
902                                escaped: false,
903                            };
904                        }
905                        b'"' => {
906                            self.pos += 1;
907                            self.state = SplitState::StringScalar {
908                                start,
909                                escaped: false,
910                            };
911                        }
912                        _ => {
913                            self.state = SplitState::Primitive { start };
914                        }
915                    }
916                }
917                SplitState::Container {
918                    start,
919                    mut depth,
920                    mut in_string,
921                    mut escaped,
922                } => {
923                    while self.pos < self.buf.len() {
924                        let byte = self.buf[self.pos];
925                        self.pos += 1;
926                        if in_string {
927                            if escaped {
928                                escaped = false;
929                            } else if byte == b'\\' {
930                                escaped = true;
931                            } else if byte == b'"' {
932                                in_string = false;
933                            }
934                        } else {
935                            match byte {
936                                b'"' => in_string = true,
937                                b'{' | b'[' => depth += 1,
938                                b'}' | b']' => {
939                                    depth -= 1;
940                                    if depth == 0 {
941                                        let value = self.buf[start..self.pos].to_vec();
942                                        self.state = SplitState::Idle;
943                                        return Ok(Some(value));
944                                    }
945                                }
946                                _ => {}
947                            }
948                        }
949                    }
950                    // Buffer exhausted mid-container; preserve state for the
951                    // next chunk.
952                    self.state = SplitState::Container {
953                        start,
954                        depth,
955                        in_string,
956                        escaped,
957                    };
958                    return Ok(None);
959                }
960                SplitState::StringScalar { start, mut escaped } => {
961                    while self.pos < self.buf.len() {
962                        let byte = self.buf[self.pos];
963                        self.pos += 1;
964                        if escaped {
965                            escaped = false;
966                        } else if byte == b'\\' {
967                            escaped = true;
968                        } else if byte == b'"' {
969                            let value = self.buf[start..self.pos].to_vec();
970                            self.state = SplitState::Idle;
971                            return Ok(Some(value));
972                        }
973                    }
974                    self.state = SplitState::StringScalar { start, escaped };
975                    return Ok(None);
976                }
977                SplitState::Primitive { start } => {
978                    while self.pos < self.buf.len() {
979                        let byte = self.buf[self.pos];
980                        if Self::is_ws(byte) || matches!(byte, b',' | b']' | b'}') {
981                            let value = self.buf[start..self.pos].to_vec();
982                            self.state = SplitState::Idle;
983                            return Ok(Some(value));
984                        }
985                        self.pos += 1;
986                    }
987                    if self.eof {
988                        // EOF terminates a trailing primitive.
989                        let value = self.buf[start..self.pos].to_vec();
990                        self.state = SplitState::Idle;
991                        if value.is_empty() {
992                            return Ok(None);
993                        }
994                        return Ok(Some(value));
995                    }
996                    self.state = SplitState::Primitive { start };
997                    return Ok(None);
998                }
999            }
1000        }
1001    }
1002
1003    /// Validate that the stream ended on a value boundary. A residual
1004    /// in-progress value (unclosed container / string) means the input was
1005    /// truncated at a chunk boundary — a fail-loud error rather than silent
1006    /// data loss.
1007    fn finish(&self) -> Result<(), JsonLdParseError> {
1008        match self.state {
1009            SplitState::Idle => {
1010                if self.buf[self.pos..].iter().any(|b| !Self::is_ws(*b)) {
1011                    return Err(JsonLdParseError::ProcessingError(
1012                        "trailing non-whitespace bytes after the final JSON-LD value".to_string(),
1013                    ));
1014                }
1015                Ok(())
1016            }
1017            _ => Err(JsonLdParseError::ProcessingError(
1018                "incomplete JSON-LD document: input ended in the middle of a value \
1019                 (truncated at a chunk boundary or malformed JSON)"
1020                    .to_string(),
1021            )),
1022        }
1023    }
1024}
1025
1026/// Streaming processing statistics
1027#[derive(Debug, Clone)]
1028pub struct StreamingStatistics {
1029    pub total_bytes_processed: usize,
1030    pub total_triples_parsed: usize,
1031    pub processing_time: Duration,
1032    pub average_throughput_mbps: f64,
1033    pub parse_errors: usize,
1034    pub context_cache_hit_ratio: f64,
1035    pub simd_operations_count: usize,
1036    pub zero_copy_operations_count: usize,
1037}
1038
1039impl PerformanceMonitor {
1040    fn new() -> Self {
1041        Self {
1042            total_bytes_processed: AtomicUsize::new(0),
1043            total_triples_parsed: AtomicUsize::new(0),
1044            parse_errors: AtomicUsize::new(0),
1045            context_cache_hits: AtomicUsize::new(0),
1046            context_cache_misses: AtomicUsize::new(0),
1047            simd_operations: AtomicUsize::new(0),
1048            zero_copy_operations: AtomicUsize::new(0),
1049            start_time: Instant::now(),
1050            chunk_processing_times: Arc::new(Mutex::new(VecDeque::with_capacity(1000))),
1051        }
1052    }
1053
1054    fn record_bytes_processed(&self, bytes: usize) {
1055        self.total_bytes_processed
1056            .fetch_add(bytes, Ordering::Relaxed);
1057    }
1058
1059    fn record_triples_parsed(&self, count: usize) {
1060        self.total_triples_parsed
1061            .fetch_add(count, Ordering::Relaxed);
1062    }
1063
1064    fn should_flush_batch(&self) -> bool {
1065        // Adaptive flushing logic based on performance metrics
1066        self.average_chunk_processing_time() > Duration::from_millis(100)
1067    }
1068
1069    fn average_chunk_processing_time(&self) -> Duration {
1070        let times = self.chunk_processing_times.lock();
1071        if times.is_empty() {
1072            return Duration::from_millis(1);
1073        }
1074
1075        let total: Duration = times.iter().sum();
1076        total / times.len() as u32
1077    }
1078
1079    fn memory_pressure_detected(&self) -> bool {
1080        // Simplified memory pressure detection
1081        false // Implementation would check actual memory usage
1082    }
1083
1084    fn get_statistics(&self) -> StreamingStatistics {
1085        let elapsed = self.start_time.elapsed();
1086        let bytes = self.total_bytes_processed.load(Ordering::Relaxed);
1087        let triples = self.total_triples_parsed.load(Ordering::Relaxed);
1088        let errors = self.parse_errors.load(Ordering::Relaxed);
1089        let cache_hits = self.context_cache_hits.load(Ordering::Relaxed);
1090        let cache_misses = self.context_cache_misses.load(Ordering::Relaxed);
1091        let simd_ops = self.simd_operations.load(Ordering::Relaxed);
1092        let zero_copy_ops = self.zero_copy_operations.load(Ordering::Relaxed);
1093
1094        let throughput_mbps = if elapsed.as_secs() > 0 {
1095            (bytes as f64) / (1024.0 * 1024.0) / elapsed.as_secs_f64()
1096        } else {
1097            0.0
1098        };
1099
1100        let cache_hit_ratio = if cache_hits + cache_misses > 0 {
1101            cache_hits as f64 / (cache_hits + cache_misses) as f64
1102        } else {
1103            0.0
1104        };
1105
1106        StreamingStatistics {
1107            total_bytes_processed: bytes,
1108            total_triples_parsed: triples,
1109            processing_time: elapsed,
1110            average_throughput_mbps: throughput_mbps,
1111            parse_errors: errors,
1112            context_cache_hit_ratio: cache_hit_ratio,
1113            simd_operations_count: simd_ops,
1114            zero_copy_operations_count: zero_copy_ops,
1115        }
1116    }
1117}
1118
1119impl BufferPool {
1120    fn new(buffer_size: usize, max_buffers: usize) -> Self {
1121        Self {
1122            available_buffers: Arc::new(Mutex::new(Vec::with_capacity(max_buffers))),
1123            buffer_size,
1124            max_buffers,
1125            current_buffers: AtomicUsize::new(0),
1126        }
1127    }
1128
1129    async fn get_buffer(&self) -> ZeroCopyBuffer {
1130        loop {
1131            // Try to get a buffer without waiting
1132            {
1133                let mut buffers = self.available_buffers.lock();
1134                if let Some(buffer) = buffers.pop() {
1135                    return buffer;
1136                }
1137            } // MutexGuard dropped here
1138
1139            if self.current_buffers.load(Ordering::Relaxed) < self.max_buffers {
1140                self.current_buffers.fetch_add(1, Ordering::Relaxed);
1141                return ZeroCopyBuffer::new(self.buffer_size);
1142            } else {
1143                // Wait for a buffer to become available
1144                tokio::time::sleep(Duration::from_millis(1)).await;
1145            }
1146        }
1147    }
1148
1149    #[allow(dead_code)]
1150    fn return_buffer(&self, mut buffer: ZeroCopyBuffer) {
1151        buffer.reset();
1152        let mut buffers = self.available_buffers.lock();
1153        if buffers.len() < self.max_buffers {
1154            buffers.push(buffer);
1155        } else {
1156            self.current_buffers.fetch_sub(1, Ordering::Relaxed);
1157        }
1158    }
1159}
1160
1161/// Memory-efficient sink that accumulates triples in memory
1162pub struct MemoryStreamingSink {
1163    triples: Arc<RwLock<Vec<Triple>>>,
1164    quads: Arc<RwLock<Vec<Quad>>>,
1165    statistics: Arc<RwLock<SinkStatistics>>,
1166}
1167
1168impl Default for MemoryStreamingSink {
1169    fn default() -> Self {
1170        Self::new()
1171    }
1172}
1173
1174impl MemoryStreamingSink {
1175    pub fn new() -> Self {
1176        Self {
1177            triples: Arc::new(RwLock::new(Vec::new())),
1178            quads: Arc::new(RwLock::new(Vec::new())),
1179            statistics: Arc::new(RwLock::new(SinkStatistics {
1180                total_triples_processed: 0,
1181                total_quads_processed: 0,
1182                average_batch_size: 0.0,
1183                processing_rate_per_second: 0.0,
1184                memory_usage_bytes: 0,
1185            })),
1186        }
1187    }
1188
1189    pub fn into_triples(self) -> Arc<RwLock<Vec<Triple>>> {
1190        self.triples
1191    }
1192
1193    pub async fn get_triples(&self) -> Vec<Triple> {
1194        self.triples.read().await.clone()
1195    }
1196
1197    pub async fn get_quads(&self) -> Vec<Quad> {
1198        self.quads.read().await.clone()
1199    }
1200}
1201
1202/// Error type for streaming operations
1203#[derive(Debug)]
1204pub struct StreamingError(Box<dyn StdError + Send + Sync>);
1205
1206impl std::fmt::Display for StreamingError {
1207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1208        write!(f, "Streaming error: {}", self.0)
1209    }
1210}
1211
1212impl StdError for StreamingError {
1213    fn source(&self) -> Option<&(dyn StdError + 'static)> {
1214        Some(&*self.0)
1215    }
1216}
1217
1218impl From<Box<dyn StdError + Send + Sync>> for StreamingError {
1219    fn from(err: Box<dyn StdError + Send + Sync>) -> Self {
1220        StreamingError(err)
1221    }
1222}
1223
1224#[async_trait::async_trait]
1225impl StreamingSink for MemoryStreamingSink {
1226    type Error = StreamingError;
1227
1228    async fn process_triple_batch(&mut self, triples: Vec<Triple>) -> Result<(), Self::Error> {
1229        let batch_size = triples.len();
1230        self.triples.write().await.extend(triples);
1231
1232        let mut stats = self.statistics.write().await;
1233        stats.total_triples_processed += batch_size;
1234        stats.average_batch_size = (stats.average_batch_size + batch_size as f64) / 2.0;
1235
1236        Ok(())
1237    }
1238
1239    async fn process_quad_batch(&mut self, quads: Vec<Quad>) -> Result<(), Self::Error> {
1240        let batch_size = quads.len();
1241        self.quads.write().await.extend(quads);
1242
1243        let mut stats = self.statistics.write().await;
1244        stats.total_quads_processed += batch_size;
1245
1246        Ok(())
1247    }
1248
1249    async fn flush(&mut self) -> Result<(), Self::Error> {
1250        // Memory sink doesn't need explicit flushing
1251        Ok(())
1252    }
1253
1254    fn performance_statistics(&self) -> SinkStatistics {
1255        // Would need to implement actual memory usage calculation
1256        SinkStatistics {
1257            total_triples_processed: 0,
1258            total_quads_processed: 0,
1259            average_batch_size: 0.0,
1260            processing_rate_per_second: 0.0,
1261            memory_usage_bytes: 0,
1262        }
1263    }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::*;
1269    use crate::model::Predicate;
1270    use std::io::Cursor;
1271
1272    /// Collect (predicate_iri, object_string) pairs from parsed triples.
1273    fn predicates(triples: &[Triple]) -> Vec<String> {
1274        triples
1275            .iter()
1276            .map(|t| match t.predicate() {
1277                Predicate::NamedNode(n) => n.as_str().to_string(),
1278                Predicate::Variable(v) => v.as_str().to_string(),
1279            })
1280            .collect()
1281    }
1282
1283    async fn run_parse(
1284        config: StreamingConfig,
1285        data: &str,
1286    ) -> Result<Vec<Triple>, JsonLdParseError> {
1287        let mut parser = UltraStreamingJsonLdParser::new(config);
1288        let reader = Cursor::new(data.as_bytes().to_vec());
1289        let sink = MemoryStreamingSink::new();
1290        let triples_arc = Arc::clone(&sink.triples);
1291        parser.stream_parse(reader, sink).await?;
1292        let triples = triples_arc.read().await.clone();
1293        Ok(triples)
1294    }
1295
1296    async fn run_parse_with_loader(
1297        config: StreamingConfig,
1298        data: &str,
1299        loader: Arc<StreamingDocumentLoader>,
1300    ) -> Result<Vec<Triple>, JsonLdParseError> {
1301        let mut parser = UltraStreamingJsonLdParser::new(config).with_document_loader(loader);
1302        let reader = Cursor::new(data.as_bytes().to_vec());
1303        let sink = MemoryStreamingSink::new();
1304        let triples_arc = Arc::clone(&sink.triples);
1305        parser.stream_parse(reader, sink).await?;
1306        let triples = triples_arc.read().await.clone();
1307        Ok(triples)
1308    }
1309
1310    #[tokio::test]
1311    async fn regression_streaming_array_multiple_objects() {
1312        // Array of node objects with absolute-IRI predicates (no context
1313        // needed). Verifies both the array handling and correct IRIs.
1314        let json = r#"[
1315            {"@id": "http://example.org/person/1", "http://schema.org/name": "Alice"},
1316            {"@id": "http://example.org/person/2", "http://schema.org/name": "Bob"}
1317        ]"#;
1318
1319        let triples = run_parse(StreamingConfig::default(), json)
1320            .await
1321            .expect("array document should parse");
1322        assert_eq!(triples.len(), 2, "expected one triple per object");
1323        for p in predicates(&triples) {
1324            assert_eq!(p, "http://schema.org/name");
1325        }
1326    }
1327
1328    #[tokio::test]
1329    async fn regression_large_object_split_across_chunks() {
1330        // A single object with many properties, parsed with a tiny buffer so the
1331        // byte stream is split at arbitrary offsets (mid-object / mid-string).
1332        // Previously every chunk was parsed independently as a whole JSON
1333        // document and this failed with a serde parse error.
1334        let mut props = String::new();
1335        for i in 0..60 {
1336            props.push_str(&format!(
1337                ",\n  \"prop{i}\": \"value number {i} with spaces\""
1338            ));
1339        }
1340        let json = format!(
1341            "{{\n  \"@context\": {{\"@vocab\": \"http://example.com/vocab#\"}},\n  \
1342             \"@id\": \"http://example.org/subject\"{props}\n}}"
1343        );
1344
1345        // Small read buffer forces the ~2.6 KB document to be split across many
1346        // chunk boundaries (mid-object / mid-string). Kept above ~doc_len/100 so
1347        // the fixed-capacity buffer pool is not exhausted.
1348        let config = StreamingConfig {
1349            buffer_size: 64,
1350            enable_simd: false,
1351            ..Default::default()
1352        };
1353
1354        let triples = run_parse(config, &json)
1355            .await
1356            .expect("large document split across chunks should parse");
1357        assert_eq!(triples.len(), 60, "all 60 properties should yield triples");
1358        for p in predicates(&triples) {
1359            assert!(
1360                p.starts_with("http://example.com/vocab#prop"),
1361                "predicate should be @vocab-expanded, got {p}"
1362            );
1363        }
1364    }
1365
1366    #[tokio::test]
1367    async fn regression_large_object_split_across_chunks_simd() {
1368        // Same as above but exercising the SIMD processing path.
1369        let mut props = String::new();
1370        for i in 0..40 {
1371            props.push_str(&format!(",\n  \"p{i}\": \"v{i}\""));
1372        }
1373        let json = format!(
1374            "{{\"@context\": {{\"@vocab\": \"http://ex.com/v#\"}}, \
1375             \"@id\": \"http://example.org/s\"{props}}}"
1376        );
1377
1378        let config = StreamingConfig {
1379            buffer_size: 8,
1380            enable_simd: true,
1381            ..Default::default()
1382        };
1383
1384        let triples = run_parse(config, &json)
1385            .await
1386            .expect("SIMD path large document should parse");
1387        assert_eq!(triples.len(), 40);
1388    }
1389
1390    #[tokio::test]
1391    async fn regression_expand_property_no_fabricated_namespace() {
1392        // Unmapped terms must NOT silently become http://example.org/<term>.
1393        // With @vocab they expand correctly; a term mapped in the context uses
1394        // its mapping.
1395        let json = r#"{
1396            "@context": {
1397                "@vocab": "http://example.com/vocab#",
1398                "name": "http://schema.org/name"
1399            },
1400            "@id": "http://example.org/s",
1401            "name": "Alice",
1402            "age": "30"
1403        }"#;
1404
1405        let triples = run_parse(StreamingConfig::default(), json)
1406            .await
1407            .expect("document with @vocab should parse");
1408        let preds = predicates(&triples);
1409        assert!(
1410            preds.contains(&"http://schema.org/name".to_string()),
1411            "explicit term mapping must win: {preds:?}"
1412        );
1413        assert!(
1414            preds.contains(&"http://example.com/vocab#age".to_string()),
1415            "unmapped term must use @vocab: {preds:?}"
1416        );
1417        assert!(
1418            !preds.iter().any(|p| p.contains("http://example.org/age")),
1419            "no fabricated example.org namespace allowed: {preds:?}"
1420        );
1421    }
1422
1423    #[tokio::test]
1424    async fn regression_unmapped_term_without_vocab_fails_loud() {
1425        // No @vocab, no term definition, no colon -> cannot expand. Must be an
1426        // explicit error, never a fabricated IRI and never a silent success.
1427        let json = r#"{"@id": "http://example.org/s", "name": "Alice"}"#;
1428        let result = run_parse(StreamingConfig::default(), json).await;
1429        assert!(
1430            result.is_err(),
1431            "unmappable term must fail loudly, got {result:?}"
1432        );
1433    }
1434
1435    #[tokio::test]
1436    async fn regression_remote_context_without_loader_fails_loud() {
1437        // String @context is a remote reference. Without a configured loader we
1438        // must fail loudly instead of substituting an empty context.
1439        let json = r#"{"@context": "https://schema.org/", "@id": "http://example.org/s", "name": "Alice"}"#;
1440        let result = run_parse(StreamingConfig::default(), json).await;
1441        assert!(
1442            result.is_err(),
1443            "remote @context without loader must fail loudly, got {result:?}"
1444        );
1445    }
1446
1447    #[tokio::test]
1448    async fn regression_remote_context_resolved_with_loader() {
1449        // With a loader, the remote context is fetched and used to expand terms
1450        // to their real IRIs (not http://example.org/...).
1451        let json = r#"{"@context": "https://example.test/ctx", "@id": "http://example.org/s", "name": "Alice"}"#;
1452        let loader: Arc<StreamingDocumentLoader> =
1453            Arc::new(|iri: &str, _opts: &JsonLdLoadDocumentOptions| {
1454                assert_eq!(iri, "https://example.test/ctx");
1455                Ok(JsonLdRemoteDocument {
1456                    document: br#"{"@context": {"name": "https://schema.org/name"}}"#.to_vec(),
1457                    document_url: iri.to_string(),
1458                })
1459            });
1460
1461        let triples = run_parse_with_loader(StreamingConfig::default(), json, loader)
1462            .await
1463            .expect("remote context should resolve via loader");
1464        let preds = predicates(&triples);
1465        assert_eq!(preds, vec!["https://schema.org/name".to_string()]);
1466    }
1467
1468    #[tokio::test]
1469    async fn regression_remote_context_loader_failure_propagates() {
1470        // A loader that errors must surface the error, not be swallowed.
1471        let json = r#"{"@context": "https://broken.test/ctx", "@id": "http://example.org/s", "name": "Alice"}"#;
1472        let loader: Arc<StreamingDocumentLoader> =
1473            Arc::new(|_iri: &str, _opts: &JsonLdLoadDocumentOptions| {
1474                Err::<JsonLdRemoteDocument, _>("network unreachable".into())
1475            });
1476
1477        let result = run_parse_with_loader(StreamingConfig::default(), json, loader).await;
1478        assert!(result.is_err(), "loader failure must propagate: {result:?}");
1479    }
1480
1481    #[tokio::test]
1482    async fn regression_truncated_document_fails_loud() {
1483        // Input truncated mid-object (never closed). Must error, not silently
1484        // produce zero triples with success.
1485        let json = r#"[{"@id": "http://example.org/1", "http://schema.org/name": "Alice"}"#; // missing ']'
1486        let result = run_parse(StreamingConfig::default(), json).await;
1487        assert!(
1488            result.is_err(),
1489            "truncated document must fail loudly, got {result:?}"
1490        );
1491    }
1492
1493    #[test]
1494    fn regression_splitter_reassembles_across_chunk_boundaries() {
1495        // Feed a JSON object one byte at a time; the splitter must yield exactly
1496        // one complete value with all bytes intact.
1497        let doc = br#"{"a": "b\"c", "d": [1, 2, {"e": "}"}]}"#;
1498        let mut splitter = TopLevelJsonSplitter::new();
1499        let mut out = Vec::new();
1500        for byte in doc.iter() {
1501            splitter.push(&[*byte]);
1502            while let Some(v) = splitter
1503                .next_complete_value()
1504                .expect("splitter should not error on valid input")
1505            {
1506                out.push(v);
1507            }
1508        }
1509        splitter.mark_eof();
1510        while let Some(v) = splitter
1511            .next_complete_value()
1512            .expect("splitter should not error at eof")
1513        {
1514            out.push(v);
1515        }
1516        splitter
1517            .finish()
1518            .expect("valid document should finish cleanly");
1519        assert_eq!(out.len(), 1);
1520        assert_eq!(out[0], doc.to_vec());
1521    }
1522
1523    #[test]
1524    fn regression_splitter_yields_multiple_concatenated_values() {
1525        // Newline-delimited JSON objects must be emitted individually.
1526        let doc = b"{\"a\":1}\n{\"b\":2}\n[3,4]";
1527        let mut splitter = TopLevelJsonSplitter::new();
1528        splitter.push(doc);
1529        splitter.mark_eof();
1530        let mut out = Vec::new();
1531        while let Some(v) = splitter.next_complete_value().expect("no error") {
1532            out.push(String::from_utf8(v).expect("utf8"));
1533        }
1534        splitter.finish().expect("clean finish");
1535        assert_eq!(out, vec!["{\"a\":1}", "{\"b\":2}", "[3,4]"]);
1536    }
1537
1538    #[test]
1539    fn regression_splitter_incomplete_container_errors_on_finish() {
1540        let mut splitter = TopLevelJsonSplitter::new();
1541        splitter.push(b"{\"a\": [1, 2");
1542        splitter.mark_eof();
1543        while splitter
1544            .next_complete_value()
1545            .expect("no error while draining")
1546            .is_some()
1547        {}
1548        assert!(
1549            splitter.finish().is_err(),
1550            "unclosed container must be reported as truncated"
1551        );
1552    }
1553}