1use std::collections::HashMap;
10use std::io::Write;
11use std::ops::{Bound, Range};
12use std::path::{Path, PathBuf};
13use std::time::{Instant, SystemTime, UNIX_EPOCH};
14
15use crate::branch::{BranchCatalog, BranchInfo, BranchName, BranchStatus};
16use crate::commit::CommitPolicy;
17use crate::event::{Body, Event};
18use crate::format::{
19 derive_stream_id, generate_id_bytes, BatchId, BranchId, CodecId, EventId, EventType, Metadata,
20 OwnedStoredRecord, RecordEnvelopeV2, StreamId, StreamRevision,
21};
22use crate::log::reader::{FrameFilter, ResolvedFilter};
23use crate::log::{Log, LogReader, RecordReader, ReplayEnd, ReplayPlan, StreamSelector};
24use crate::projection::{decode_stored_event, replay_into, NamespaceScoped, Projection};
25use crate::stream::{event_fingerprint, StreamCatalog};
26use crate::view::{catch_up, View};
27use crate::{
28 AppendReceipt, AppendRequest, Durability, ExpectedRevision, ReceiptDurability, Result,
29 SalamanderError, StreamName,
30};
31
32pub struct Salamander<B> {
40 pub(crate) log: Log,
41 views: HashMap<String, Box<dyn View<B>>>,
45 policy: CommitPolicy,
48 pending_bytes: u64,
49 pending_count: u64,
50 last_commit: Instant,
51 catalog: StreamCatalog,
52 branches: BranchCatalog,
53 durable_head: u64,
54 root: PathBuf,
55}
56
57fn load_core_catalog(root: &Path, database_id: [u8; 16], head: u64) -> Option<StreamCatalog> {
58 let bytes = std::fs::read(root.join("core-catalog.bin")).ok()?;
59 if bytes.len() > 256 * 1024 * 1024 {
60 return None;
61 }
62 let checkpoint: CoreCatalogCheckpoint = bincode::deserialize(&bytes).ok()?;
63 if checkpoint.database_id != database_id || checkpoint.head != head {
64 return None;
65 }
66 let payload =
67 bincode::serialize(&(checkpoint.database_id, checkpoint.head, &checkpoint.catalog)).ok()?;
68 (crc32c::crc32c(&payload) == checkpoint.checksum).then_some(checkpoint.catalog)
69}
70
71fn persist_core_catalog(
72 root: &Path,
73 database_id: [u8; 16],
74 head: u64,
75 catalog: &StreamCatalog,
76) -> Result<()> {
77 let payload = bincode::serialize(&(database_id, head, catalog))
78 .map_err(|error| SalamanderError::Serialization(error.to_string()))?;
79 let checkpoint = CoreCatalogCheckpoint {
80 database_id,
81 head,
82 catalog: catalog.clone(),
83 checksum: crc32c::crc32c(&payload),
84 };
85 let bytes = bincode::serialize(&checkpoint)
86 .map_err(|error| SalamanderError::Serialization(error.to_string()))?;
87 let temporary = root.join("core-catalog.tmp");
88 let final_path = root.join("core-catalog.bin");
89 {
90 let mut file = std::fs::File::create(&temporary)?;
91 file.write_all(&bytes)?;
92 file.sync_all()?;
93 }
94 std::fs::rename(temporary, final_path)?;
95 Ok(())
96}
97
98#[derive(serde::Serialize, serde::Deserialize)]
99struct CoreCatalogCheckpoint {
100 database_id: [u8; 16],
101 head: u64,
102 catalog: StreamCatalog,
103 checksum: u32,
104}
105
106impl<B: Body> Salamander<B> {
107 pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
116 Self::open_with_policy(dir, CommitPolicy::default())
117 }
118
119 pub fn open_with_policy(dir: impl AsRef<Path>, policy: CommitPolicy) -> Result<Self> {
123 let root = dir.as_ref().to_path_buf();
124 let log = Log::open(&root)?;
125 let catalog = match load_core_catalog(&root, log.database_id().into_bytes(), log.head()) {
126 Some(catalog) => catalog,
127 None => {
128 let catalog = StreamCatalog::rebuild(log.records_from(0))?;
129 let _ = persist_core_catalog(
132 &root,
133 log.database_id().into_bytes(),
134 log.head(),
135 &catalog,
136 );
137 catalog
138 }
139 };
140 let branches = BranchCatalog::rebuild(log.system_records())?;
141 let durable_head = log.head();
142 Ok(Salamander {
143 log,
144 views: HashMap::new(),
145 policy,
146 pending_bytes: 0,
147 pending_count: 0,
148 last_commit: Instant::now(),
149 catalog,
150 branches,
151 durable_head,
152 root,
153 })
154 }
155
156 pub fn set_commit_policy(&mut self, policy: CommitPolicy) {
160 self.policy = policy;
161 }
162
163 pub fn commit_policy(&self) -> CommitPolicy {
165 self.policy
166 }
167
168 pub fn append(&mut self, namespace: &str, body: B) -> Result<u64> {
172 let request = AppendRequest {
173 branch: BranchId::ZERO,
174 stream: crate::StreamName::new(namespace)?,
175 expected: ExpectedRevision::Any,
176 idempotency_key: None,
177 events: vec![crate::NewEvent::new(
178 EventType::new(std::any::type_name::<B>())?,
179 body,
180 )],
181 durability: Durability::Buffered,
182 };
183 Ok(self.append_batch(request)?.first_position)
184 }
185
186 pub fn append_on_branch(&mut self, branch: BranchId, namespace: &str, body: B) -> Result<u64> {
188 let request = AppendRequest {
189 branch,
190 stream: crate::StreamName::new(namespace)?,
191 expected: ExpectedRevision::Any,
192 idempotency_key: None,
193 events: vec![crate::NewEvent::new(
194 EventType::new(std::any::type_name::<B>())?,
195 body,
196 )],
197 durability: Durability::Buffered,
198 };
199 Ok(self.append_batch(request)?.first_position)
200 }
201
202 #[allow(dead_code)]
203 fn append_wp01_compat(&mut self, namespace: &str, body: B) -> Result<u64> {
204 let timestamp_ms = current_timestamp_ms();
205 let mut event = Event {
206 offset: 0,
207 timestamp_ms,
208 namespace: namespace.to_string(),
209 body,
210 };
211 let bytes = bincode::serialize(&event.body)
212 .map_err(|e| SalamanderError::Serialization(e.to_string()))?;
213 let database_id = self.log.database_id();
214 let branch_id = BranchId::ZERO;
215 let id_bytes = generate_id_bytes();
216 let mut metadata = Metadata::new();
217 metadata.insert(
218 "salamander.stream_name".to_string(),
219 namespace.as_bytes().to_vec(),
220 );
221 let envelope = RecordEnvelopeV2 {
222 event_id: EventId::from_bytes(id_bytes),
223 database_id,
224 branch_id,
225 stream_id: derive_stream_id(database_id, branch_id, namespace),
226 stream_revision: StreamRevision(self.log.head()),
230 timestamp_unix_nanos: (timestamp_ms as i64).saturating_mul(1_000_000),
231 event_type: EventType::new(std::any::type_name::<B>())?,
232 schema_version: 1,
233 codec: CodecId::RUST_BINCODE_V1,
234 batch_id: BatchId::from_bytes(id_bytes),
235 batch_index: 0,
236 metadata,
237 };
238 let (offset, last) = self.log.append_batch(&[(envelope, bytes.clone())])?;
239 debug_assert_eq!(offset, last);
240
241 event.offset = offset;
248 for view in self.views.values_mut() {
249 view.apply(&event);
250 }
251
252 self.pending_bytes += bytes.len() as u64;
255 self.pending_count += 1;
256 if self.policy.should_commit(
257 self.pending_bytes,
258 self.pending_count,
259 self.last_commit.elapsed(),
260 ) {
261 self.commit()?;
262 }
263
264 self.catalog = StreamCatalog::rebuild(self.log.records_from(0))?;
265 if self.durable_head == self.log.head() {
266 persist_core_catalog(
267 &self.root,
268 self.log.database_id().into_bytes(),
269 self.durable_head,
270 &self.catalog,
271 )?;
272 }
273 Ok(offset)
274 }
275
276 pub fn append_batch(&mut self, request: AppendRequest<B>) -> Result<AppendReceipt> {
280 self.append_batch_with_id(request, None)
281 }
282
283 pub(crate) fn append_batch_with_id(
284 &mut self,
285 request: AppendRequest<B>,
286 supplied_batch_id: Option<BatchId>,
287 ) -> Result<AppendReceipt> {
288 request.validate()?;
289 let branch = self
290 .branches
291 .get(request.branch)
292 .ok_or_else(|| SalamanderError::BranchNotFound(format!("{:?}", request.branch)))?;
293 if branch.status == BranchStatus::Archived {
294 return Err(SalamanderError::BranchArchived(
295 branch.name.as_str().to_string(),
296 ));
297 }
298 let previous = self.catalog.revision(request.branch, &request.stream);
299 let serialized: Vec<Vec<u8>> = request
300 .events
301 .iter()
302 .map(|event| {
303 bincode::serialize(&event.body)
304 .map_err(|error| SalamanderError::Serialization(error.to_string()))
305 })
306 .collect::<Result<_>>()?;
307 let request_digest = request_fingerprint(&request, &serialized);
308 if let Some(key) = &request.idempotency_key {
309 if let Some((digest, mut receipt)) = self.catalog.idempotent(request.branch, key) {
310 if digest != request_digest {
311 return Err(SalamanderError::IdempotencyConflict);
312 }
313 if request.durability == Durability::Sync
314 && receipt.durability != ReceiptDurability::Synced
315 {
316 self.commit()?;
317 receipt.durability = ReceiptDurability::Synced;
318 }
319 return Ok(receipt);
320 }
321 }
322 validate_expected(request.expected, previous)?;
323
324 let database_id = self.log.database_id();
325 let stream_id = self
326 .catalog
327 .stream_id(request.branch, &request.stream)
328 .unwrap_or_else(|| {
329 derive_stream_id(database_id, request.branch, request.stream.as_str())
330 });
331 let batch_id =
332 supplied_batch_id.unwrap_or_else(|| BatchId::from_bytes(generate_id_bytes()));
333 let first_revision = previous.map_or(0, |revision| revision.0 + 1);
334 let timestamp_ms = current_timestamp_ms();
335 let mut stored = Vec::with_capacity(request.events.len());
336 let mut digests = Vec::with_capacity(request.events.len());
337
338 for (index, (event, body)) in request.events.iter().zip(&serialized).enumerate() {
339 let event_id = event
340 .event_id
341 .unwrap_or_else(|| EventId::from_bytes(generate_id_bytes()));
342 let mut metadata = event.metadata.clone();
343 metadata.insert(
344 "salamander.stream_name".into(),
345 request.stream.as_str().as_bytes().to_vec(),
346 );
347 if let Some(key) = &request.idempotency_key {
348 metadata.insert("salamander.idempotency_key".into(), key.as_bytes().to_vec());
349 metadata.insert(
350 "salamander.request_digest".into(),
351 request_digest.to_le_bytes().to_vec(),
352 );
353 }
354 let envelope = RecordEnvelopeV2 {
355 event_id,
356 database_id,
357 branch_id: request.branch,
358 stream_id,
359 stream_revision: StreamRevision(first_revision + index as u64),
360 timestamp_unix_nanos: (timestamp_ms as i64).saturating_mul(1_000_000),
361 event_type: event.event_type.clone(),
362 schema_version: event.schema_version,
363 codec: CodecId::RUST_BINCODE_V1,
364 batch_id,
365 batch_index: index as u32,
366 metadata,
367 };
368 let record = crate::format::OwnedStoredRecord {
369 kind: crate::format::FrameKind::Event,
370 flags: 0,
371 position: self.log.head() + index as u64,
372 envelope: envelope.clone(),
373 payload: body.clone(),
374 };
375 let digest = event_fingerprint(&record);
376 if self
377 .catalog
378 .event_digest(event_id)
379 .is_some_and(|old| old != digest)
380 || digests
381 .iter()
382 .any(|(id, old)| *id == event_id && *old != digest)
383 {
384 return Err(SalamanderError::EventIdConflict);
385 }
386 digests.push((event_id, digest));
387 stored.push((envelope, body.clone()));
388 }
389
390 let supplied_ids: Vec<_> = request.events.iter().map(|event| event.event_id).collect();
391 if supplied_ids
392 .iter()
393 .flatten()
394 .any(|id| self.catalog.event_receipt(*id).is_some())
395 {
396 let mut original: Option<AppendReceipt> = None;
397 for (supplied, (_, digest)) in supplied_ids.iter().zip(&digests) {
398 let Some(id) = supplied else {
399 return Err(SalamanderError::EventIdConflict);
400 };
401 let Some((stored_digest, receipt)) = self.catalog.event_receipt(*id) else {
402 return Err(SalamanderError::EventIdConflict);
403 };
404 if stored_digest != *digest
405 || original
406 .as_ref()
407 .is_some_and(|existing| existing.batch_id != receipt.batch_id)
408 {
409 return Err(SalamanderError::EventIdConflict);
410 }
411 original = Some(receipt);
412 }
413 let mut original = original.ok_or(SalamanderError::EventIdConflict)?;
414 if supplied_batch_id.is_some_and(|id| id != original.batch_id) {
415 return Err(SalamanderError::BatchIdConflict);
416 }
417 if request.durability == Durability::Sync
418 && original.durability != ReceiptDurability::Synced
419 {
420 self.commit()?;
421 original.durability = ReceiptDurability::Synced;
422 }
423 return Ok(original);
424 }
425
426 if self.catalog.batch_receipt(batch_id).is_some() {
427 return Err(SalamanderError::BatchIdConflict);
428 }
429
430 let (first_position, last_position) = self.log.append_batch(&stored)?;
431 for (index, event) in request.events.iter().enumerate() {
432 let runtime = Event {
433 offset: first_position + index as u64,
434 timestamp_ms,
435 namespace: request.stream.as_str().to_string(),
436 body: event.body.clone(),
437 };
438 for view in self.views.values_mut() {
439 view.apply(&runtime);
440 }
441 }
442
443 self.pending_bytes += serialized.iter().map(Vec::len).sum::<usize>() as u64;
444 self.pending_count += request.events.len() as u64;
445 let sync = request.durability == Durability::Sync
446 || self.policy.should_commit(
447 self.pending_bytes,
448 self.pending_count,
449 self.last_commit.elapsed(),
450 );
451 if sync {
452 self.commit()?;
453 }
454 let receipt = AppendReceipt {
455 batch_id,
456 first_position,
457 last_position,
458 stream_id,
459 previous_revision: previous,
460 current_revision: StreamRevision(first_revision + request.events.len() as u64 - 1),
461 durability: if sync {
462 ReceiptDurability::Synced
463 } else if request.durability == Durability::Flush {
464 ReceiptDurability::Flushed
465 } else {
466 ReceiptDurability::Buffered
467 },
468 };
469 self.catalog.record_batch(
470 request.branch,
471 &request.stream,
472 stream_id,
473 digests,
474 request
475 .idempotency_key
476 .as_ref()
477 .map(|key| (key, request_digest)),
478 receipt.clone(),
479 );
480 if sync {
481 persist_core_catalog(
482 &self.root,
483 self.log.database_id().into_bytes(),
484 self.durable_head,
485 &self.catalog,
486 )?;
487 }
488 Ok(receipt)
489 }
490
491 pub fn branch(&self, id: BranchId) -> Option<&BranchInfo> {
493 self.branches.get(id)
494 }
495
496 pub fn branch_named(&self, name: &str) -> Option<&BranchInfo> {
498 self.branches.named(name)
499 }
500
501 pub fn branch_ancestry(&self, id: BranchId) -> Result<Vec<BranchInfo>> {
503 self.branches.ancestry(id)
504 }
505
506 pub fn branch_children(&self, id: BranchId) -> Vec<BranchInfo> {
508 self.branches.children(id)
509 }
510
511 pub fn branch_common_ancestor(&self, left: BranchId, right: BranchId) -> Result<BranchInfo> {
513 self.branches.common_ancestor(left, right)
514 }
515
516 pub fn diff(&self, request: DiffRequest) -> Result<TimelineDiff> {
525 request.streams.validate()?;
526 let head = self.log.head();
527 let resolve = |end: ReplayEnd| match end {
528 ReplayEnd::Head => Ok(head),
529 ReplayEnd::At(position) if position > head => {
530 Err(SalamanderError::OffsetBeyondHead(position))
531 }
532 ReplayEnd::At(position) => Ok(position),
533 };
534 let left_until = resolve(request.left_until)?;
535 let right_until = resolve(request.right_until)?;
536 let branch = |id: BranchId| {
537 self.branches
538 .get(id)
539 .cloned()
540 .ok_or_else(|| SalamanderError::BranchNotFound(format!("{id:?}")))
541 };
542 let left = branch(request.left)?;
543 let right = branch(request.right)?;
544 let (common_ancestor, divergence) =
545 self.branches
546 .divergence(request.left, left_until, request.right, right_until)?;
547 let plan = |branch: BranchId, from: u64, until: u64| ReplayPlan {
548 branch,
549 streams: request.streams.clone(),
550 from: Bound::Included(from),
551 until: ReplayEnd::At(until),
552 ..ReplayPlan::default()
553 };
554 Ok(TimelineDiff {
555 shared: plan(common_ancestor.id, 0, divergence),
556 common_ancestor,
557 divergence,
558 left: DiffSide {
559 suffix: plan(left.id, divergence, left_until),
560 branch: left,
561 until: left_until,
562 },
563 right: DiffSide {
564 suffix: plan(right.id, divergence, right_until),
565 branch: right,
566 until: right_until,
567 },
568 })
569 }
570
571 pub fn read(&self, plan: ReplayPlan) -> Result<LogReader<'_>> {
577 plan.streams.validate()?;
578 let head = self.log.head();
579 let until = match plan.until {
580 ReplayEnd::Head => head,
581 ReplayEnd::At(position) => {
582 if position > head {
583 return Err(SalamanderError::OffsetBeyondHead(position));
584 }
585 position
586 }
587 };
588 let from = match plan.from {
589 Bound::Unbounded => 0,
590 Bound::Included(position) => position,
591 Bound::Excluded(position) => position.saturating_add(1),
592 };
593 let scopes = self.branches.replay_scopes(plan.branch, until)?;
594 Ok(self.log.plan_reader(ResolvedFilter {
595 from,
596 until,
597 selector: plan.streams,
598 scopes: Some(scopes),
599 time: plan.time,
600 kinds: FrameFilter::UserEvents,
601 max_events: plan.max_events,
602 verification: plan.verification,
603 }))
604 }
605
606 pub fn replay_branch(
610 &self,
611 branch: BranchId,
612 namespace: &str,
613 range: Range<u64>,
614 mut f: impl FnMut(&Event<B>),
615 ) -> Result<()> {
616 let mut reader = self.read(ReplayPlan {
617 branch,
618 from: Bound::Included(range.start),
619 until: ReplayEnd::At(range.end),
620 ..ReplayPlan::default()
621 })?;
622 while let Some(record) = reader.next()? {
623 let record = OwnedStoredRecord::from(record);
624 let event = decode_stored_event::<B>(&record)?;
625 if event.namespace == namespace {
626 f(&event);
627 }
628 }
629 Ok(())
630 }
631
632 pub fn fork_branch(
637 &mut self,
638 parent: BranchId,
639 at: u64,
640 name: BranchName,
641 metadata: Metadata,
642 ) -> Result<BranchInfo> {
643 if self.branches.get(parent).is_none() {
644 return Err(SalamanderError::BranchNotFound(format!("{parent:?}")));
645 }
646 if at > self.head() {
647 return Err(SalamanderError::OffsetBeyondHead(at));
648 }
649 if !self.is_batch_boundary(at)? {
650 return Err(SalamanderError::NotBatchBoundary(at));
651 }
652 let id = BranchId::from_bytes(generate_id_bytes());
653 let info = BranchInfo {
654 id,
655 name,
656 parent: Some(parent),
657 fork_position: Some(at),
658 created_at_unix_nanos: (current_timestamp_ms() as i64).saturating_mul(1_000_000),
659 metadata,
660 status: BranchStatus::Active,
661 };
662 let mut updated_branches = self.branches.clone();
665 updated_branches.insert(info.clone())?;
666 let payload = serde_json::to_vec(&info)
667 .map_err(|error| SalamanderError::Serialization(error.to_string()))?;
668 let event_bytes = generate_id_bytes();
669 let envelope = RecordEnvelopeV2 {
670 event_id: EventId::from_bytes(event_bytes),
671 database_id: self.log.database_id(),
672 branch_id: id,
673 stream_id: crate::StreamId::ZERO,
674 stream_revision: StreamRevision(0),
675 timestamp_unix_nanos: info.created_at_unix_nanos,
676 event_type: EventType::new("salamander.branch.created")?,
677 schema_version: 1,
678 codec: CodecId::JSON_UTF8,
679 batch_id: BatchId::from_bytes(event_bytes),
680 batch_index: 0,
681 metadata: Metadata::new(),
682 };
683 self.log.append_system(&envelope, &payload)?;
684 if let Err(error) = self.commit() {
685 self.branches = BranchCatalog::rebuild(self.log.system_records())?;
686 return Err(error);
687 }
688 self.branches = updated_branches;
689 Ok(info)
690 }
691
692 pub fn archive_branch(&mut self, id: BranchId) -> Result<BranchInfo> {
695 let mut info = self
696 .branches
697 .get(id)
698 .cloned()
699 .ok_or_else(|| SalamanderError::BranchNotFound(format!("{id:?}")))?;
700 if id == BranchId::ZERO {
701 return Err(SalamanderError::InvalidArgument(
702 "the default branch cannot be archived".into(),
703 ));
704 }
705 if info.status == BranchStatus::Archived {
706 return Ok(info);
707 }
708 info.status = BranchStatus::Archived;
709 let mut updated_branches = self.branches.clone();
710 updated_branches.archive(info.clone())?;
711 let payload = serde_json::to_vec(&info)
712 .map_err(|error| SalamanderError::Serialization(error.to_string()))?;
713 let event_bytes = generate_id_bytes();
714 let envelope = RecordEnvelopeV2 {
715 event_id: EventId::from_bytes(event_bytes),
716 database_id: self.log.database_id(),
717 branch_id: id,
718 stream_id: crate::StreamId::ZERO,
719 stream_revision: StreamRevision(0),
720 timestamp_unix_nanos: (current_timestamp_ms() as i64).saturating_mul(1_000_000),
721 event_type: EventType::new("salamander.branch.archived")?,
722 schema_version: 1,
723 codec: CodecId::JSON_UTF8,
724 batch_id: BatchId::from_bytes(event_bytes),
725 batch_index: 0,
726 metadata: Metadata::new(),
727 };
728 self.log.append_system(&envelope, &payload)?;
729 if let Err(error) = self.commit() {
730 self.branches = BranchCatalog::rebuild(self.log.system_records())?;
731 return Err(error);
732 }
733 self.branches = updated_branches;
734 Ok(info)
735 }
736
737 fn is_batch_boundary(&self, at: u64) -> Result<bool> {
738 if at == 0 || at == self.head() {
739 return Ok(true);
740 }
741 let mut before = None;
744 let mut after = None;
745 for item in self.log.records_from(at - 1) {
746 let record = item?;
747 if record.position == at - 1 {
748 before = Some(record.envelope.batch_id);
749 } else if record.position >= at {
750 after = (record.position == at).then_some(record.envelope.batch_id);
751 break;
752 }
753 }
754 Ok(matches!((before, after), (Some(left), Some(right)) if left != right))
755 }
756
757 pub fn commit(&mut self) -> Result<u64> {
761 let head = self.log.commit()?;
762 self.durable_head = head;
763 self.pending_bytes = 0;
764 self.pending_count = 0;
765 self.last_commit = Instant::now();
766 persist_core_catalog(
767 &self.root,
768 self.log.database_id().into_bytes(),
769 head,
770 &self.catalog,
771 )?;
772 Ok(head)
773 }
774
775 pub fn uncommitted_bytes(&self) -> u64 {
778 self.pending_bytes
779 }
780
781 pub fn uncommitted_count(&self) -> u64 {
783 self.pending_count
784 }
785
786 pub fn projection<P: Projection<Body = B> + Default>(&self) -> Result<P> {
790 let mut p = P::default();
791 replay_into(&mut p, &self.log, self.log.head())?;
792 Ok(p)
793 }
794
795 pub fn projection_for<P: NamespaceScoped<Body = B>>(&self, namespace: &str) -> Result<P> {
801 let mut p = P::new_for(namespace);
802 replay_into(&mut p, &self.log, self.log.head())?;
803 Ok(p)
804 }
805
806 pub fn view_at<P: Projection<Body = B> + Default>(&self, n: u64) -> Result<P> {
810 if n > self.log.head() {
811 return Err(SalamanderError::OffsetBeyondHead(n));
812 }
813 let mut p = P::default();
814 replay_into(&mut p, &self.log, n)?;
815 Ok(p)
816 }
817
818 pub fn register(&mut self, name: &str, mut view: Box<dyn View<B>>) -> Result<()> {
824 catch_up(view.as_mut(), &self.log, self.log.head())?;
825 self.views.insert(name.to_string(), view);
826 Ok(())
827 }
828
829 pub fn deregister(&mut self, name: &str) -> Option<Box<dyn View<B>>> {
833 self.views.remove(name)
834 }
835
836 pub fn view<T: View<B>>(&self, name: &str) -> Option<&T> {
844 self.views.get(name)?.as_any().downcast_ref::<T>()
845 }
846
847 pub fn replay_to<P: Projection<Body = B>>(&self, mut view: P, n: u64) -> Result<P> {
853 if n > self.log.head() {
854 return Err(SalamanderError::OffsetBeyondHead(n));
855 }
856 replay_into(&mut view, &self.log, n)?;
857 Ok(view)
858 }
859
860 pub fn head(&self) -> u64 {
862 self.log.head()
863 }
864
865 pub fn durable_head(&self) -> u64 {
867 self.durable_head
868 }
869
870 pub(crate) fn stream_id(&self, branch: BranchId, stream: &StreamName) -> Option<StreamId> {
871 self.catalog.stream_id(branch, stream)
872 }
873
874 pub fn replay(
876 &self,
877 namespace: &str,
878 range: Range<u64>,
879 f: impl FnMut(&Event<B>),
880 ) -> Result<()> {
881 crate::introspect::replay(&self.log, namespace, range, f)
882 }
883}
884
885#[derive(Debug, Clone, PartialEq, Eq)]
889pub struct DiffRequest {
890 pub left: BranchId,
892 pub right: BranchId,
894 pub left_until: ReplayEnd,
896 pub right_until: ReplayEnd,
898 pub streams: StreamSelector,
901}
902
903impl DiffRequest {
904 pub fn new(left: BranchId, right: BranchId) -> Self {
906 Self {
907 left,
908 right,
909 left_until: ReplayEnd::Head,
910 right_until: ReplayEnd::Head,
911 streams: StreamSelector::All,
912 }
913 }
914}
915
916#[derive(Debug, Clone, PartialEq, Eq)]
919pub struct DiffSide {
920 pub branch: BranchInfo,
922 pub until: u64,
924 pub suffix: ReplayPlan,
926}
927
928#[derive(Debug, Clone, PartialEq, Eq)]
933pub struct TimelineDiff {
934 pub common_ancestor: BranchInfo,
936 pub divergence: u64,
938 pub shared: ReplayPlan,
942 pub left: DiffSide,
944 pub right: DiffSide,
946}
947
948fn current_timestamp_ms() -> u64 {
949 SystemTime::now()
950 .duration_since(UNIX_EPOCH)
951 .map(|d| d.as_millis() as u64)
952 .unwrap_or(0)
953}
954
955fn validate_expected(expected: ExpectedRevision, actual: Option<StreamRevision>) -> Result<()> {
956 let matches = match expected {
957 ExpectedRevision::Any => true,
958 ExpectedRevision::NoStream => actual.is_none(),
959 ExpectedRevision::Exact(expected) => actual == Some(expected),
960 };
961 if matches {
962 return Ok(());
963 }
964 Err(SalamanderError::RevisionConflict {
965 expected: format!("{expected:?}"),
966 actual: format!("{actual:?}"),
967 })
968}
969
970fn request_fingerprint<B>(request: &AppendRequest<B>, bodies: &[Vec<u8>]) -> u32 {
971 let mut bytes = Vec::new();
972 bytes.extend_from_slice(request.branch.as_bytes());
973 bytes.extend_from_slice(request.stream.as_str().as_bytes());
974 bytes.extend_from_slice(format!("{:?}", request.expected).as_bytes());
975 for (event, body) in request.events.iter().zip(bodies) {
976 bytes.extend_from_slice(event.event_id.unwrap_or(EventId::ZERO).as_bytes());
977 bytes.extend_from_slice(event.event_type.as_str().as_bytes());
978 bytes.extend_from_slice(&event.schema_version.to_le_bytes());
979 for (key, value) in &event.metadata {
980 bytes.extend_from_slice(key.as_bytes());
981 bytes.extend_from_slice(value);
982 }
983 bytes.extend_from_slice(body);
984 }
985 crc32c::crc32c(&bytes)
986}