Skip to main content

sectorsync_core/
component.rs

1//! Custom component registry and station-local blob storage.
2
3use std::collections::{BTreeMap, HashMap};
4
5use crate::ids::{ComponentId, EntityHandle};
6use crate::spatial::Vec3;
7
8/// Component codec error used by built-in codecs.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ComponentCodecError {
11    /// Input length did not match codec expectation.
12    ExpectedBytes {
13        /// Expected byte count.
14        expected: usize,
15        /// Actual byte count.
16        actual: usize,
17    },
18}
19
20impl core::fmt::Display for ComponentCodecError {
21    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22        match self {
23            Self::ExpectedBytes { expected, actual } => {
24                write!(f, "expected {expected} bytes, got {actual}")
25            }
26        }
27    }
28}
29
30impl std::error::Error for ComponentCodecError {}
31
32/// Typed component codec. Embedders can implement this for their own compact
33/// schema and bit-packing formats.
34pub trait ComponentCodec<T> {
35    /// Encodes `value` into `out`.
36    fn encode(&self, value: &T, out: &mut Vec<u8>) -> Result<(), ComponentCodecError>;
37
38    /// Decodes a value from bytes.
39    fn decode(&self, input: &[u8]) -> Result<T, ComponentCodecError>;
40
41    /// Fixed encoded size when known.
42    fn fixed_size(&self) -> Option<usize> {
43        None
44    }
45}
46
47/// Little-endian `u32` codec.
48#[derive(Clone, Copy, Debug, Default)]
49pub struct U32LeCodec;
50
51impl ComponentCodec<u32> for U32LeCodec {
52    fn encode(&self, value: &u32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
53        out.extend_from_slice(&value.to_le_bytes());
54        Ok(())
55    }
56
57    fn decode(&self, input: &[u8]) -> Result<u32, ComponentCodecError> {
58        let bytes = exact_array::<4>(input)?;
59        Ok(u32::from_le_bytes(bytes))
60    }
61
62    fn fixed_size(&self) -> Option<usize> {
63        Some(4)
64    }
65}
66
67/// Little-endian `f32` codec.
68#[derive(Clone, Copy, Debug, Default)]
69pub struct F32LeCodec;
70
71impl ComponentCodec<f32> for F32LeCodec {
72    fn encode(&self, value: &f32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
73        out.extend_from_slice(&value.to_le_bytes());
74        Ok(())
75    }
76
77    fn decode(&self, input: &[u8]) -> Result<f32, ComponentCodecError> {
78        let bytes = exact_array::<4>(input)?;
79        Ok(f32::from_le_bytes(bytes))
80    }
81
82    fn fixed_size(&self) -> Option<usize> {
83        Some(4)
84    }
85}
86
87/// Little-endian `Vec3` codec.
88#[derive(Clone, Copy, Debug, Default)]
89pub struct Vec3LeCodec;
90
91impl ComponentCodec<Vec3> for Vec3LeCodec {
92    fn encode(&self, value: &Vec3, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
93        out.extend_from_slice(&value.x.to_le_bytes());
94        out.extend_from_slice(&value.y.to_le_bytes());
95        out.extend_from_slice(&value.z.to_le_bytes());
96        Ok(())
97    }
98
99    fn decode(&self, input: &[u8]) -> Result<Vec3, ComponentCodecError> {
100        if input.len() != 12 {
101            return Err(ComponentCodecError::ExpectedBytes {
102                expected: 12,
103                actual: input.len(),
104            });
105        }
106        let x = f32::from_le_bytes(input[0..4].try_into().expect("slice length checked"));
107        let y = f32::from_le_bytes(input[4..8].try_into().expect("slice length checked"));
108        let z = f32::from_le_bytes(input[8..12].try_into().expect("slice length checked"));
109        Ok(Vec3 { x, y, z })
110    }
111
112    fn fixed_size(&self) -> Option<usize> {
113        Some(12)
114    }
115}
116
117fn exact_array<const N: usize>(input: &[u8]) -> Result<[u8; N], ComponentCodecError> {
118    if input.len() != N {
119        return Err(ComponentCodecError::ExpectedBytes {
120            expected: N,
121            actual: input.len(),
122        });
123    }
124    let mut out = [0_u8; N];
125    out.copy_from_slice(input);
126    Ok(out)
127}
128
129/// Storage strategy declared by a registered component.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum ComponentStorageKind {
132    /// `SectorSync` stores opaque component bytes in a sparse station-local column.
133    SparseBlob,
134    /// Component data lives outside `SectorSync`; the registry only documents it.
135    External,
136}
137
138/// Synchronization behavior declared by a registered component.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum ComponentSyncMode {
141    /// Component is never replicated by `SectorSync`.
142    NotReplicated,
143    /// Component is replicated as delta when dirty.
144    Delta,
145    /// Component is sent as a snapshot when selected by policy.
146    Snapshot,
147    /// Component changes are represented by events.
148    EventOnly,
149}
150
151/// Migration behavior declared by a registered component.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum ComponentMigrationMode {
154    /// Copy component bytes during owner handoff.
155    Copy,
156    /// Drop component bytes during owner handoff.
157    Drop,
158    /// External system owns migration.
159    External,
160}
161
162/// Registered component descriptor.
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct ComponentDescriptor {
165    /// Component id used in hot-path records.
166    pub id: ComponentId,
167    /// Stable debug name.
168    pub name: &'static str,
169    /// Storage strategy.
170    pub storage: ComponentStorageKind,
171    /// Synchronization strategy.
172    pub sync: ComponentSyncMode,
173    /// Migration strategy.
174    pub migration: ComponentMigrationMode,
175    /// Maximum accepted blob size in bytes for `SectorSync`-owned storage.
176    pub max_bytes: usize,
177    /// Stable schema hash selected by the embedding application.
178    pub schema_hash: u64,
179}
180
181impl ComponentDescriptor {
182    /// Creates a sparse blob descriptor.
183    pub const fn sparse_blob(
184        id: ComponentId,
185        name: &'static str,
186        sync: ComponentSyncMode,
187        migration: ComponentMigrationMode,
188        max_bytes: usize,
189    ) -> Self {
190        Self {
191            id,
192            name,
193            storage: ComponentStorageKind::SparseBlob,
194            sync,
195            migration,
196            max_bytes,
197            schema_hash: 0,
198        }
199    }
200
201    /// Attaches a stable schema hash to this descriptor.
202    #[must_use]
203    pub const fn with_schema_hash(mut self, schema_hash: u64) -> Self {
204        self.schema_hash = schema_hash;
205        self
206    }
207}
208
209/// Typed component schema descriptor.
210#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct ComponentSchema {
212    /// Component descriptor.
213    pub descriptor: ComponentDescriptor,
214    /// Fixed encoded size when known.
215    pub fixed_size: Option<usize>,
216}
217
218impl ComponentSchema {
219    /// Creates a typed schema from a descriptor and codec.
220    pub fn new<T, C: ComponentCodec<T>>(descriptor: ComponentDescriptor, codec: &C) -> Self {
221        Self {
222            descriptor,
223            fixed_size: codec.fixed_size(),
224        }
225    }
226}
227
228/// Fixed field type used by generated component schema helpers.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum ComponentFieldType {
231    /// Unsigned 8-bit integer.
232    U8,
233    /// Unsigned 16-bit little-endian integer.
234    U16,
235    /// Unsigned 32-bit little-endian integer.
236    U32,
237    /// Unsigned 64-bit little-endian integer.
238    U64,
239    /// Signed 32-bit little-endian integer.
240    I32,
241    /// 32-bit little-endian floating point value.
242    F32,
243    /// Three little-endian `f32` values.
244    Vec3,
245    /// Opaque bytes with a maximum generated layout size.
246    Bytes {
247        /// Maximum byte count reserved in the generated layout.
248        max_len: usize,
249    },
250}
251
252impl ComponentFieldType {
253    /// Maximum encoded size in bytes.
254    pub const fn max_size(self) -> usize {
255        match self {
256            Self::U8 => 1,
257            Self::U16 => 2,
258            Self::U32 | Self::I32 | Self::F32 => 4,
259            Self::U64 => 8,
260            Self::Vec3 => 12,
261            Self::Bytes { max_len } => max_len,
262        }
263    }
264
265    const fn tag(self) -> u8 {
266        match self {
267            Self::U8 => 1,
268            Self::U16 => 2,
269            Self::U32 => 3,
270            Self::U64 => 4,
271            Self::I32 => 5,
272            Self::F32 => 6,
273            Self::Vec3 => 7,
274            Self::Bytes { .. } => 8,
275        }
276    }
277}
278
279/// Field descriptor emitted by external schema generators.
280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub struct ComponentFieldDescriptor {
282    /// Stable field name.
283    pub name: &'static str,
284    /// Encoded field type.
285    pub ty: ComponentFieldType,
286    /// Byte offset inside the generated component blob.
287    pub offset: usize,
288}
289
290impl ComponentFieldDescriptor {
291    /// Creates a generated field descriptor.
292    pub const fn new(name: &'static str, ty: ComponentFieldType, offset: usize) -> Self {
293        Self { name, ty, offset }
294    }
295
296    /// Returns the exclusive end offset.
297    pub const fn end_offset(self) -> usize {
298        self.offset.saturating_add(self.ty.max_size())
299    }
300}
301
302/// Component schema shape emitted by an external generator.
303#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct GeneratedComponentSchema {
305    /// Component id used in hot-path records.
306    pub id: ComponentId,
307    /// Stable debug name.
308    pub name: &'static str,
309    /// Storage strategy.
310    pub storage: ComponentStorageKind,
311    /// Synchronization strategy.
312    pub sync: ComponentSyncMode,
313    /// Migration strategy.
314    pub migration: ComponentMigrationMode,
315    /// Maximum accepted blob size in bytes.
316    pub max_bytes: usize,
317    /// Generated field layout.
318    pub fields: &'static [ComponentFieldDescriptor],
319}
320
321impl GeneratedComponentSchema {
322    /// Creates a generated component schema.
323    pub const fn new(
324        id: ComponentId,
325        name: &'static str,
326        storage: ComponentStorageKind,
327        sync: ComponentSyncMode,
328        migration: ComponentMigrationMode,
329        max_bytes: usize,
330        fields: &'static [ComponentFieldDescriptor],
331    ) -> Self {
332        Self {
333            id,
334            name,
335            storage,
336            sync,
337            migration,
338            max_bytes,
339            fields,
340        }
341    }
342
343    /// Validates generated layout invariants.
344    pub fn validate(&self) -> Result<(), ComponentSchemaError> {
345        for (index, field) in self.fields.iter().enumerate() {
346            if field.end_offset() > self.max_bytes {
347                return Err(ComponentSchemaError::FieldOutOfBounds {
348                    name: field.name,
349                    offset: field.offset,
350                    size: field.ty.max_size(),
351                    max_bytes: self.max_bytes,
352                });
353            }
354
355            for earlier in &self.fields[..index] {
356                if earlier.name == field.name {
357                    return Err(ComponentSchemaError::DuplicateFieldName(field.name));
358                }
359                if ranges_overlap(
360                    earlier.offset,
361                    earlier.end_offset(),
362                    field.offset,
363                    field.end_offset(),
364                ) {
365                    return Err(ComponentSchemaError::FieldOverlap {
366                        left: earlier.name,
367                        right: field.name,
368                    });
369                }
370            }
371        }
372        Ok(())
373    }
374
375    /// Returns the generated schema hash.
376    pub fn schema_hash(&self) -> u64 {
377        let mut hash = FNV_OFFSET;
378        hash = hash_u64(hash, self.id.get().into());
379        hash = hash_str(hash, self.name);
380        hash = hash_u8(hash, storage_tag(self.storage));
381        hash = hash_u8(hash, sync_tag(self.sync));
382        hash = hash_u8(hash, migration_tag(self.migration));
383        hash = hash_u64(hash, self.max_bytes as u64);
384        for field in self.fields {
385            hash = hash_str(hash, field.name);
386            hash = hash_u8(hash, field.ty.tag());
387            hash = hash_u64(hash, field.ty.max_size() as u64);
388            hash = hash_u64(hash, field.offset as u64);
389        }
390        hash
391    }
392
393    /// Returns the maximum generated fixed size.
394    pub fn fixed_size(&self) -> Option<usize> {
395        self.fields
396            .iter()
397            .map(|field| field.end_offset())
398            .max()
399            .or(Some(0))
400    }
401
402    /// Builds a component descriptor with the generated schema hash attached.
403    pub fn descriptor(&self) -> ComponentDescriptor {
404        ComponentDescriptor {
405            id: self.id,
406            name: self.name,
407            storage: self.storage,
408            sync: self.sync,
409            migration: self.migration,
410            max_bytes: self.max_bytes,
411            schema_hash: self.schema_hash(),
412        }
413    }
414
415    /// Builds a typed component schema wrapper.
416    pub fn component_schema(&self) -> ComponentSchema {
417        ComponentSchema {
418            descriptor: self.descriptor(),
419            fixed_size: self.fixed_size(),
420        }
421    }
422}
423
424/// Generated component schema validation error.
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub enum ComponentSchemaError {
427    /// Field name was repeated.
428    DuplicateFieldName(&'static str),
429    /// Field range exceeded `max_bytes`.
430    FieldOutOfBounds {
431        /// Field name.
432        name: &'static str,
433        /// Field offset.
434        offset: usize,
435        /// Field size.
436        size: usize,
437        /// Maximum component bytes.
438        max_bytes: usize,
439    },
440    /// Two field byte ranges overlap.
441    FieldOverlap {
442        /// Earlier field.
443        left: &'static str,
444        /// Later field.
445        right: &'static str,
446    },
447}
448
449impl core::fmt::Display for ComponentSchemaError {
450    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
451        match self {
452            Self::DuplicateFieldName(name) => write!(f, "duplicate component field {name}"),
453            Self::FieldOutOfBounds {
454                name,
455                offset,
456                size,
457                max_bytes,
458            } => write!(
459                f,
460                "component field {name} at {offset} with size {size} exceeds max bytes {max_bytes}"
461            ),
462            Self::FieldOverlap { left, right } => {
463                write!(f, "component fields {left} and {right} overlap")
464            }
465        }
466    }
467}
468
469impl std::error::Error for ComponentSchemaError {}
470
471/// Component registry error.
472#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum ComponentRegistryError {
474    /// Component id is already registered.
475    DuplicateId(ComponentId),
476    /// Component name is already registered.
477    DuplicateName(&'static str),
478}
479
480impl core::fmt::Display for ComponentRegistryError {
481    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
482        match self {
483            Self::DuplicateId(id) => write!(f, "duplicate component id {}", id.get()),
484            Self::DuplicateName(name) => write!(f, "duplicate component name {name}"),
485        }
486    }
487}
488
489impl std::error::Error for ComponentRegistryError {}
490
491/// Error produced while registering a generated schema.
492#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum GeneratedSchemaRegistrationError {
494    /// Generated schema validation failed.
495    Schema(ComponentSchemaError),
496    /// Component registry rejected the generated descriptor.
497    Registry(ComponentRegistryError),
498}
499
500impl core::fmt::Display for GeneratedSchemaRegistrationError {
501    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
502        match self {
503            Self::Schema(error) => write!(f, "{error}"),
504            Self::Registry(error) => write!(f, "{error}"),
505        }
506    }
507}
508
509impl std::error::Error for GeneratedSchemaRegistrationError {
510    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
511        match self {
512            Self::Schema(error) => Some(error),
513            Self::Registry(error) => Some(error),
514        }
515    }
516}
517
518/// Dense component descriptor registry.
519#[derive(Clone, Debug, Default)]
520pub struct ComponentRegistry {
521    descriptors: Vec<Option<ComponentDescriptor>>,
522}
523
524impl ComponentRegistry {
525    /// Registers a component descriptor.
526    pub fn register(
527        &mut self,
528        descriptor: ComponentDescriptor,
529    ) -> Result<(), ComponentRegistryError> {
530        if self.get(descriptor.id).is_some() {
531            return Err(ComponentRegistryError::DuplicateId(descriptor.id));
532        }
533        if self.iter().any(|existing| existing.name == descriptor.name) {
534            return Err(ComponentRegistryError::DuplicateName(descriptor.name));
535        }
536
537        let index = usize::from(descriptor.id.get());
538        if self.descriptors.len() <= index {
539            self.descriptors.resize(index + 1, None);
540        }
541        self.descriptors[index] = Some(descriptor);
542        Ok(())
543    }
544
545    /// Validates and registers a generated component schema.
546    pub fn register_generated_schema(
547        &mut self,
548        schema: &GeneratedComponentSchema,
549    ) -> Result<ComponentSchema, GeneratedSchemaRegistrationError> {
550        schema
551            .validate()
552            .map_err(GeneratedSchemaRegistrationError::Schema)?;
553        let component_schema = schema.component_schema();
554        self.register(component_schema.descriptor.clone())
555            .map_err(GeneratedSchemaRegistrationError::Registry)?;
556        Ok(component_schema)
557    }
558
559    /// Gets a descriptor by component id.
560    pub fn get(&self, id: ComponentId) -> Option<&ComponentDescriptor> {
561        self.descriptors
562            .get(usize::from(id.get()))
563            .and_then(Option::as_ref)
564    }
565
566    /// Iterates over descriptors.
567    pub fn iter(&self) -> impl Iterator<Item = &ComponentDescriptor> {
568        self.descriptors.iter().filter_map(Option::as_ref)
569    }
570
571    /// Number of registered descriptors.
572    pub fn len(&self) -> usize {
573        self.iter().count()
574    }
575
576    /// Returns whether no descriptors are registered.
577    pub fn is_empty(&self) -> bool {
578        self.len() == 0
579    }
580}
581
582const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
583const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
584
585fn hash_u8(hash: u64, value: u8) -> u64 {
586    (hash ^ u64::from(value)).wrapping_mul(FNV_PRIME)
587}
588
589fn hash_u64(mut hash: u64, value: u64) -> u64 {
590    for byte in value.to_le_bytes() {
591        hash = hash_u8(hash, byte);
592    }
593    hash
594}
595
596fn hash_str(mut hash: u64, value: &str) -> u64 {
597    for byte in value.bytes() {
598        hash = hash_u8(hash, byte);
599    }
600    hash_u8(hash, 0)
601}
602
603fn storage_tag(storage: ComponentStorageKind) -> u8 {
604    match storage {
605        ComponentStorageKind::SparseBlob => 1,
606        ComponentStorageKind::External => 2,
607    }
608}
609
610fn sync_tag(sync: ComponentSyncMode) -> u8 {
611    match sync {
612        ComponentSyncMode::NotReplicated => 0,
613        ComponentSyncMode::Delta => 1,
614        ComponentSyncMode::Snapshot => 2,
615        ComponentSyncMode::EventOnly => 3,
616    }
617}
618
619fn migration_tag(migration: ComponentMigrationMode) -> u8 {
620    match migration {
621        ComponentMigrationMode::Copy => 1,
622        ComponentMigrationMode::Drop => 2,
623        ComponentMigrationMode::External => 3,
624    }
625}
626
627fn ranges_overlap(
628    left_start: usize,
629    left_end: usize,
630    right_start: usize,
631    right_end: usize,
632) -> bool {
633    left_start < right_end && right_start < left_end
634}
635
636/// Opaque component blob stored in a station-local component column.
637#[derive(Clone, Debug, Default, PartialEq, Eq)]
638pub struct ComponentBlob {
639    /// Monotonic version selected by the writer.
640    pub version: u64,
641    /// Dirty flag used by replication planners.
642    pub dirty: bool,
643    /// Opaque bytes.
644    pub bytes: Vec<u8>,
645}
646
647/// Caller-owned reusable storage for typed component encoding.
648#[derive(Clone, Debug, Default)]
649pub struct ComponentEncodeScratch {
650    bytes: Vec<u8>,
651}
652
653impl ComponentEncodeScratch {
654    /// Creates empty encoding scratch.
655    pub const fn new() -> Self {
656        Self { bytes: Vec::new() }
657    }
658
659    /// Creates encoding scratch with an initial byte capacity.
660    pub fn with_capacity(capacity: usize) -> Self {
661        Self {
662            bytes: Vec::with_capacity(capacity),
663        }
664    }
665
666    /// Retained byte capacity available to subsequent encodes.
667    pub fn retained_capacity(&self) -> usize {
668        self.bytes.capacity()
669    }
670}
671
672/// Component storage error.
673#[derive(Clone, Copy, Debug, PartialEq, Eq)]
674pub enum ComponentStoreError {
675    /// Descriptor does not use `SectorSync`-owned blob storage.
676    NotBlobStorage(ComponentId),
677    /// Blob exceeds descriptor limit.
678    BlobTooLarge {
679        /// Component id.
680        component_id: ComponentId,
681        /// Blob size in bytes.
682        actual: usize,
683        /// Maximum allowed size in bytes.
684        max: usize,
685    },
686    /// Codec failed while encoding or decoding.
687    Codec(ComponentCodecError),
688    /// Component blob does not exist.
689    MissingBlob(ComponentId),
690}
691
692impl core::fmt::Display for ComponentStoreError {
693    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
694        match self {
695            Self::NotBlobStorage(id) => {
696                write!(f, "component {} is not SectorSync blob storage", id.get())
697            }
698            Self::BlobTooLarge {
699                component_id,
700                actual,
701                max,
702            } => write!(
703                f,
704                "component {} blob has {} bytes, max {}",
705                component_id.get(),
706                actual,
707                max
708            ),
709            Self::Codec(error) => write!(f, "{error}"),
710            Self::MissingBlob(id) => write!(f, "component {} blob is missing", id.get()),
711        }
712    }
713}
714
715impl std::error::Error for ComponentStoreError {}
716
717#[derive(Clone, Debug, Default)]
718struct ComponentColumn {
719    values: HashMap<EntityHandle, ComponentBlob>,
720}
721
722/// Station-local sparse component blob store.
723#[derive(Clone, Debug, Default)]
724pub struct ComponentStore {
725    dense_columns: Vec<Option<ComponentColumn>>,
726    sparse_columns: BTreeMap<ComponentId, ComponentColumn>,
727}
728
729const DENSE_COMPONENT_COLUMN_LIMIT: usize = 256;
730
731impl ComponentStore {
732    /// Reserves sparse blob entries for one component column.
733    pub fn reserve_component(&mut self, component_id: ComponentId, additional_entities: usize) {
734        self.column_mut(component_id)
735            .values
736            .reserve(additional_entities);
737    }
738
739    /// Releases unused retained column and entity-map storage.
740    pub fn reclaim_retained_capacity(&mut self) {
741        for column in self.dense_columns.iter_mut().filter_map(Option::as_mut) {
742            column.values.shrink_to_fit();
743        }
744        for column in self.sparse_columns.values_mut() {
745            column.values.shrink_to_fit();
746        }
747        self.dense_columns.shrink_to_fit();
748    }
749
750    /// Component column slots currently retained without another allocation.
751    pub fn column_slots_capacity(&self) -> usize {
752        self.dense_columns
753            .capacity()
754            .saturating_add(self.sparse_columns.len())
755    }
756
757    /// Sparse entity entries retained for one component without another rehash.
758    pub fn component_capacity(&self, component_id: ComponentId) -> usize {
759        self.column(component_id)
760            .map_or(0, |column| column.values.capacity())
761    }
762
763    /// Writes an opaque component blob.
764    pub fn set_blob(
765        &mut self,
766        descriptor: &ComponentDescriptor,
767        entity: EntityHandle,
768        version: u64,
769        bytes: Vec<u8>,
770    ) -> Result<(), ComponentStoreError> {
771        validate_blob_write(descriptor, bytes.len())?;
772
773        let column = self.column_mut(descriptor.id);
774        column.values.insert(
775            entity,
776            ComponentBlob {
777                version,
778                dirty: true,
779                bytes,
780            },
781        );
782        Ok(())
783    }
784
785    /// Copies an opaque component value into retained blob storage.
786    ///
787    /// Existing blob byte capacity is reused when sufficient. Validation runs
788    /// before mutation, so failed writes leave the previous value unchanged.
789    pub fn set_blob_from_slice(
790        &mut self,
791        descriptor: &ComponentDescriptor,
792        entity: EntityHandle,
793        version: u64,
794        bytes: &[u8],
795    ) -> Result<(), ComponentStoreError> {
796        validate_blob_write(descriptor, bytes.len())?;
797        let column = self.column_mut(descriptor.id);
798        if let Some(blob) = column.values.get_mut(&entity) {
799            blob.bytes.clear();
800            blob.bytes.extend_from_slice(bytes);
801            blob.version = version;
802            blob.dirty = true;
803        } else {
804            column.values.insert(
805                entity,
806                ComponentBlob {
807                    version,
808                    dirty: true,
809                    bytes: bytes.to_vec(),
810                },
811            );
812        }
813        Ok(())
814    }
815
816    /// Encodes and writes a typed component value using `codec`.
817    pub fn set_typed<T, C: ComponentCodec<T>>(
818        &mut self,
819        descriptor: &ComponentDescriptor,
820        entity: EntityHandle,
821        version: u64,
822        codec: &C,
823        value: &T,
824    ) -> Result<(), ComponentStoreError> {
825        let mut bytes = Vec::with_capacity(codec.fixed_size().unwrap_or(0));
826        codec
827            .encode(value, &mut bytes)
828            .map_err(ComponentStoreError::Codec)?;
829        self.set_blob(descriptor, entity, version, bytes)
830    }
831
832    /// Encodes a typed value through caller-owned scratch and copies it into
833    /// retained blob storage.
834    pub fn set_typed_with_scratch<T, C: ComponentCodec<T>>(
835        &mut self,
836        descriptor: &ComponentDescriptor,
837        entity: EntityHandle,
838        version: u64,
839        codec: &C,
840        value: &T,
841        scratch: &mut ComponentEncodeScratch,
842    ) -> Result<(), ComponentStoreError> {
843        scratch.bytes.clear();
844        if let Some(size) = codec.fixed_size() {
845            scratch.bytes.reserve(size);
846        }
847        codec
848            .encode(value, &mut scratch.bytes)
849            .map_err(ComponentStoreError::Codec)?;
850        self.set_blob_from_slice(descriptor, entity, version, &scratch.bytes)
851    }
852
853    /// Gets an opaque component blob.
854    pub fn get_blob(
855        &self,
856        component_id: ComponentId,
857        entity: EntityHandle,
858    ) -> Option<&ComponentBlob> {
859        self.column(component_id)
860            .and_then(|column| column.values.get(&entity))
861    }
862
863    /// Returns whether any selected component on `entity` is dirty.
864    ///
865    /// This is a low-level convenience for caller-owned replication eligibility
866    /// rules; the store does not clear dirty state or track per-client delivery.
867    pub fn has_dirty_selected(&self, entity: EntityHandle, component_ids: &[ComponentId]) -> bool {
868        component_ids.iter().any(|component_id| {
869            self.get_blob(*component_id, entity)
870                .is_some_and(|blob| blob.dirty)
871        })
872    }
873
874    /// Decodes a typed component value using `codec`.
875    pub fn get_typed<T, C: ComponentCodec<T>>(
876        &self,
877        component_id: ComponentId,
878        entity: EntityHandle,
879        codec: &C,
880    ) -> Result<T, ComponentStoreError> {
881        let blob = self
882            .get_blob(component_id, entity)
883            .ok_or(ComponentStoreError::MissingBlob(component_id))?;
884        codec
885            .decode(&blob.bytes)
886            .map_err(ComponentStoreError::Codec)
887    }
888
889    /// Gets a mutable opaque component blob.
890    pub fn get_blob_mut(
891        &mut self,
892        component_id: ComponentId,
893        entity: EntityHandle,
894    ) -> Option<&mut ComponentBlob> {
895        let index = usize::from(component_id.get());
896        if index < DENSE_COMPONENT_COLUMN_LIMIT {
897            self.dense_columns
898                .get_mut(index)
899                .and_then(Option::as_mut)
900                .and_then(|column| column.values.get_mut(&entity))
901        } else {
902            self.sparse_columns
903                .get_mut(&component_id)
904                .and_then(|column| column.values.get_mut(&entity))
905        }
906    }
907
908    /// Clears dirty flags for all components on one entity.
909    pub fn clear_dirty_for_entity(&mut self, entity: EntityHandle) -> usize {
910        let mut cleared = 0;
911        for column in self
912            .dense_columns
913            .iter_mut()
914            .filter_map(Option::as_mut)
915            .chain(self.sparse_columns.values_mut())
916        {
917            if let Some(blob) = column.values.get_mut(&entity)
918                && blob.dirty
919            {
920                blob.dirty = false;
921                cleared += 1;
922            }
923        }
924        cleared
925    }
926
927    /// Removes all component blobs for an entity and returns the removed values.
928    pub fn remove_entity(&mut self, entity: EntityHandle) -> Vec<(ComponentId, ComponentBlob)> {
929        let mut removed = Vec::new();
930        self.remove_entity_into(entity, &mut removed);
931        removed
932    }
933
934    /// Removes all component blobs into caller-owned reusable output storage.
935    pub fn remove_entity_into(
936        &mut self,
937        entity: EntityHandle,
938        removed: &mut Vec<(ComponentId, ComponentBlob)>,
939    ) -> usize {
940        removed.clear();
941        for (index, column) in self.dense_columns.iter_mut().enumerate() {
942            let Some(column) = column else {
943                continue;
944            };
945            if let Some(blob) = column.values.remove(&entity) {
946                removed.push((
947                    ComponentId::new(u16::try_from(index).expect("dense component id fits u16")),
948                    blob,
949                ));
950            }
951        }
952        for (component_id, column) in &mut self.sparse_columns {
953            if let Some(blob) = column.values.remove(&entity) {
954                removed.push((*component_id, blob));
955            }
956        }
957        removed.len()
958    }
959
960    /// Removes and drops all component blobs for an entity without output storage.
961    pub fn clear_entity(&mut self, entity: EntityHandle) -> usize {
962        let mut removed = 0_usize;
963        for column in self
964            .dense_columns
965            .iter_mut()
966            .filter_map(Option::as_mut)
967            .chain(self.sparse_columns.values_mut())
968        {
969            removed = removed.saturating_add(usize::from(column.values.remove(&entity).is_some()));
970        }
971        removed
972    }
973
974    /// Copies migratable component blobs from one entity handle to another.
975    pub fn copy_for_migration(
976        &mut self,
977        registry: &ComponentRegistry,
978        source: EntityHandle,
979        target: EntityHandle,
980    ) -> usize {
981        let mut copied = 0;
982        for descriptor in registry.iter() {
983            if descriptor.migration != ComponentMigrationMode::Copy {
984                continue;
985            }
986            let Some(blob) = self.get_blob(descriptor.id, source).cloned() else {
987                continue;
988            };
989            self.column_mut(descriptor.id).values.insert(target, blob);
990            copied += 1;
991        }
992        copied
993    }
994
995    /// Returns number of component blobs stored in all columns.
996    pub fn blob_count(&self) -> usize {
997        self.dense_columns
998            .iter()
999            .filter_map(Option::as_ref)
1000            .chain(self.sparse_columns.values())
1001            .map(|column| column.values.len())
1002            .sum()
1003    }
1004
1005    fn column_mut(&mut self, component_id: ComponentId) -> &mut ComponentColumn {
1006        let index = usize::from(component_id.get());
1007        if index < DENSE_COMPONENT_COLUMN_LIMIT {
1008            if self.dense_columns.len() <= index {
1009                self.dense_columns.resize_with(index + 1, || None);
1010            }
1011            self.dense_columns[index].get_or_insert_with(ComponentColumn::default)
1012        } else {
1013            self.sparse_columns.entry(component_id).or_default()
1014        }
1015    }
1016
1017    fn column(&self, component_id: ComponentId) -> Option<&ComponentColumn> {
1018        let index = usize::from(component_id.get());
1019        if index < DENSE_COMPONENT_COLUMN_LIMIT {
1020            self.dense_columns.get(index).and_then(Option::as_ref)
1021        } else {
1022            self.sparse_columns.get(&component_id)
1023        }
1024    }
1025
1026    /// Number of component columns currently registered in the store.
1027    pub fn column_count(&self) -> usize {
1028        self.dense_columns
1029            .iter()
1030            .filter(|column| column.is_some())
1031            .count()
1032            + self.sparse_columns.len()
1033    }
1034}
1035
1036fn validate_blob_write(
1037    descriptor: &ComponentDescriptor,
1038    bytes: usize,
1039) -> Result<(), ComponentStoreError> {
1040    if descriptor.storage != ComponentStorageKind::SparseBlob {
1041        return Err(ComponentStoreError::NotBlobStorage(descriptor.id));
1042    }
1043    if bytes > descriptor.max_bytes {
1044        return Err(ComponentStoreError::BlobTooLarge {
1045            component_id: descriptor.id,
1046            actual: bytes,
1047            max: descriptor.max_bytes,
1048        });
1049    }
1050    Ok(())
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056
1057    #[test]
1058    fn component_column_capacity_is_explicit_and_observable() {
1059        let component_id = ComponentId::new(3);
1060        let mut store = ComponentStore::default();
1061        store.reserve_component(component_id, 16);
1062
1063        assert!(store.column_slots_capacity() >= 1);
1064        assert_eq!(store.column_count(), 1);
1065        assert!(store.component_capacity(component_id) >= 16);
1066
1067        let descriptor = ComponentDescriptor::sparse_blob(
1068            component_id,
1069            "reserved",
1070            ComponentSyncMode::Delta,
1071            ComponentMigrationMode::Copy,
1072            4,
1073        );
1074        let handle = EntityHandle::new(1, 0);
1075        store
1076            .set_blob(&descriptor, handle, 1, vec![1, 2, 3, 4])
1077            .expect("reserved component should write");
1078        assert_eq!(
1079            store
1080                .get_blob(component_id, handle)
1081                .map(|blob| blob.bytes.as_slice()),
1082            Some(&[1, 2, 3, 4][..])
1083        );
1084    }
1085
1086    #[test]
1087    fn sparse_component_ids_only_allocate_registered_columns() {
1088        let low = ComponentId::new(1);
1089        let high = ComponentId::new(u16::MAX);
1090        let mut store = ComponentStore::default();
1091        store.reserve_component(high, 4);
1092        let capacity_after_high = store.column_slots_capacity();
1093        store.reserve_component(low, 4);
1094
1095        assert_eq!(store.column_count(), 2);
1096        assert!(capacity_after_high < 16);
1097        assert!(store.column_slots_capacity() < 16);
1098        assert!(store.component_capacity(low) >= 4);
1099        assert!(store.component_capacity(high) >= 4);
1100    }
1101
1102    #[test]
1103    fn slice_updates_reuse_blob_storage_and_reject_oversized_values_atomically() {
1104        let component_id = ComponentId::new(4);
1105        let descriptor = ComponentDescriptor::sparse_blob(
1106            component_id,
1107            "state",
1108            ComponentSyncMode::Delta,
1109            ComponentMigrationMode::Copy,
1110            8,
1111        );
1112        let handle = EntityHandle::new(1, 0);
1113        let mut store = ComponentStore::default();
1114        store
1115            .set_blob(&descriptor, handle, 1, vec![0; 8])
1116            .expect("initial blob should fit");
1117        let retained_bytes = store
1118            .get_blob(component_id, handle)
1119            .expect("blob exists")
1120            .bytes
1121            .as_ptr();
1122
1123        store
1124            .set_blob_from_slice(&descriptor, handle, 2, &[1, 2, 3, 4])
1125            .expect("slice update should fit");
1126        let blob = store.get_blob(component_id, handle).expect("blob exists");
1127        assert_eq!(blob.bytes, [1, 2, 3, 4]);
1128        assert_eq!(blob.bytes.as_ptr(), retained_bytes);
1129        assert_eq!(blob.version, 2);
1130        assert!(blob.dirty);
1131
1132        assert_eq!(
1133            store
1134                .set_blob_from_slice(&descriptor, handle, 3, &[9; 9])
1135                .expect_err("oversized update should fail"),
1136            ComponentStoreError::BlobTooLarge {
1137                component_id,
1138                actual: 9,
1139                max: 8,
1140            }
1141        );
1142        let blob = store.get_blob(component_id, handle).expect("blob remains");
1143        assert_eq!(blob.bytes, [1, 2, 3, 4]);
1144        assert_eq!(blob.version, 2);
1145    }
1146
1147    #[test]
1148    fn registry_rejects_duplicate_ids_and_names() {
1149        let mut registry = ComponentRegistry::default();
1150        let descriptor = ComponentDescriptor::sparse_blob(
1151            ComponentId::new(1),
1152            "health",
1153            ComponentSyncMode::Delta,
1154            ComponentMigrationMode::Copy,
1155            16,
1156        );
1157
1158        registry
1159            .register(descriptor.clone())
1160            .expect("first registration should work");
1161        assert_eq!(
1162            registry
1163                .register(descriptor.clone())
1164                .expect_err("duplicate id"),
1165            ComponentRegistryError::DuplicateId(ComponentId::new(1))
1166        );
1167        assert_eq!(
1168            registry
1169                .register(ComponentDescriptor::sparse_blob(
1170                    ComponentId::new(2),
1171                    "health",
1172                    ComponentSyncMode::Delta,
1173                    ComponentMigrationMode::Copy,
1174                    16,
1175                ))
1176                .expect_err("duplicate name"),
1177            ComponentRegistryError::DuplicateName("health")
1178        );
1179    }
1180
1181    #[test]
1182    fn component_store_sets_clears_and_migrates_blobs() {
1183        let descriptor = ComponentDescriptor::sparse_blob(
1184            ComponentId::new(1),
1185            "health",
1186            ComponentSyncMode::Delta,
1187            ComponentMigrationMode::Copy,
1188            16,
1189        );
1190        let mut registry = ComponentRegistry::default();
1191        registry
1192            .register(descriptor.clone())
1193            .expect("descriptor should register");
1194        let mut store = ComponentStore::default();
1195        let source = EntityHandle::new(1, 0);
1196        let target = EntityHandle::new(2, 0);
1197
1198        store
1199            .set_blob(&descriptor, source, 7, vec![1, 2, 3])
1200            .expect("blob should fit");
1201        assert!(
1202            store
1203                .get_blob(ComponentId::new(1), source)
1204                .expect("blob")
1205                .dirty
1206        );
1207        assert_eq!(store.clear_dirty_for_entity(source), 1);
1208        assert!(
1209            !store
1210                .get_blob(ComponentId::new(1), source)
1211                .expect("blob")
1212                .dirty
1213        );
1214
1215        assert_eq!(store.copy_for_migration(&registry, source, target), 1);
1216        assert_eq!(
1217            store
1218                .get_blob(ComponentId::new(1), target)
1219                .expect("target blob")
1220                .bytes,
1221            vec![1, 2, 3]
1222        );
1223    }
1224
1225    #[test]
1226    fn component_entity_removal_supports_owned_reusable_and_discard_paths() {
1227        let descriptors = [
1228            ComponentDescriptor::sparse_blob(
1229                ComponentId::new(1),
1230                "health",
1231                ComponentSyncMode::Delta,
1232                ComponentMigrationMode::Copy,
1233                8,
1234            ),
1235            ComponentDescriptor::sparse_blob(
1236                ComponentId::new(2),
1237                "armor",
1238                ComponentSyncMode::Delta,
1239                ComponentMigrationMode::Copy,
1240                8,
1241            ),
1242        ];
1243        let entities = [
1244            EntityHandle::new(1, 0),
1245            EntityHandle::new(2, 0),
1246            EntityHandle::new(3, 0),
1247            EntityHandle::new(4, 0),
1248        ];
1249        let mut store = ComponentStore::default();
1250        for entity in entities {
1251            for descriptor in &descriptors {
1252                store
1253                    .set_blob(
1254                        descriptor,
1255                        entity,
1256                        1,
1257                        vec![u8::try_from(descriptor.id.get()).expect("small component id"); 4],
1258                    )
1259                    .expect("bounded blob should write");
1260            }
1261        }
1262
1263        let owned = store.remove_entity(entities[0]);
1264        assert_eq!(owned.len(), 2);
1265        assert_eq!(owned[0].0, ComponentId::new(1));
1266        assert_eq!(owned[1].0, ComponentId::new(2));
1267
1268        let mut removed = Vec::with_capacity(2);
1269        let retained_pointer = removed.as_ptr();
1270        assert_eq!(store.remove_entity_into(entities[1], &mut removed), 2);
1271        assert_eq!(removed.as_ptr(), retained_pointer);
1272        let retained_capacity = removed.capacity();
1273        assert_eq!(store.remove_entity_into(entities[2], &mut removed), 2);
1274        assert_eq!(removed.as_ptr(), retained_pointer);
1275        assert_eq!(removed.capacity(), retained_capacity);
1276
1277        assert_eq!(store.clear_entity(entities[3]), 2);
1278        assert_eq!(store.clear_entity(entities[3]), 0);
1279        assert_eq!(store.blob_count(), 0);
1280    }
1281
1282    #[test]
1283    fn typed_component_codec_roundtrips_values() {
1284        let descriptor = ComponentDescriptor::sparse_blob(
1285            ComponentId::new(3),
1286            "velocity",
1287            ComponentSyncMode::Delta,
1288            ComponentMigrationMode::Copy,
1289            12,
1290        )
1291        .with_schema_hash(0xABCD);
1292        let schema = ComponentSchema::new(descriptor.clone(), &Vec3LeCodec);
1293        assert_eq!(schema.fixed_size, Some(12));
1294        assert_eq!(schema.descriptor.schema_hash, 0xABCD);
1295
1296        let mut store = ComponentStore::default();
1297        let entity = EntityHandle::new(7, 0);
1298        let value = Vec3::new(1.0, 2.0, 3.5);
1299
1300        store
1301            .set_typed(&descriptor, entity, 1, &Vec3LeCodec, &value)
1302            .expect("typed set should work");
1303        let decoded = store
1304            .get_typed(ComponentId::new(3), entity, &Vec3LeCodec)
1305            .expect("typed get should work");
1306        assert_eq!(decoded, value);
1307    }
1308
1309    #[test]
1310    fn typed_component_scratch_reuses_encoding_and_blob_capacity() {
1311        let component_id = ComponentId::new(5);
1312        let descriptor = ComponentDescriptor::sparse_blob(
1313            component_id,
1314            "score",
1315            ComponentSyncMode::Delta,
1316            ComponentMigrationMode::Copy,
1317            4,
1318        );
1319        let entity = EntityHandle::new(7, 0);
1320        let mut store = ComponentStore::default();
1321        let mut scratch = ComponentEncodeScratch::new();
1322
1323        store
1324            .set_typed_with_scratch(&descriptor, entity, 1, &U32LeCodec, &10, &mut scratch)
1325            .expect("initial typed write should work");
1326        let retained_scratch = scratch.retained_capacity();
1327        let retained_blob = store
1328            .get_blob(component_id, entity)
1329            .expect("blob exists")
1330            .bytes
1331            .as_ptr();
1332        store
1333            .set_typed_with_scratch(&descriptor, entity, 2, &U32LeCodec, &20, &mut scratch)
1334            .expect("repeated typed write should work");
1335
1336        assert_eq!(scratch.retained_capacity(), retained_scratch);
1337        assert_eq!(
1338            store
1339                .get_blob(component_id, entity)
1340                .expect("blob exists")
1341                .bytes
1342                .as_ptr(),
1343            retained_blob
1344        );
1345        assert_eq!(
1346            store
1347                .get_typed(component_id, entity, &U32LeCodec)
1348                .expect("typed value decodes"),
1349            20
1350        );
1351    }
1352
1353    #[test]
1354    fn selected_dirty_query_only_considers_requested_components() {
1355        let selected = ComponentDescriptor::sparse_blob(
1356            ComponentId::new(1),
1357            "selected",
1358            ComponentSyncMode::Delta,
1359            ComponentMigrationMode::Copy,
1360            4,
1361        );
1362        let ignored = ComponentDescriptor::sparse_blob(
1363            ComponentId::new(2),
1364            "ignored",
1365            ComponentSyncMode::Delta,
1366            ComponentMigrationMode::Copy,
1367            4,
1368        );
1369        let entity = EntityHandle::new(1, 0);
1370        let mut store = ComponentStore::default();
1371        store
1372            .set_blob(&selected, entity, 1, vec![1])
1373            .expect("selected blob should fit");
1374        store
1375            .set_blob(&ignored, entity, 1, vec![2])
1376            .expect("ignored blob should fit");
1377
1378        assert!(store.has_dirty_selected(entity, &[selected.id]));
1379        store
1380            .get_blob_mut(selected.id, entity)
1381            .expect("selected blob exists")
1382            .dirty = false;
1383        assert!(!store.has_dirty_selected(entity, &[selected.id]));
1384        assert!(store.has_dirty_selected(entity, &[ignored.id]));
1385        assert!(!store.has_dirty_selected(entity, &[]));
1386    }
1387
1388    #[test]
1389    fn reclaim_retained_capacity_preserves_component_blobs() {
1390        let descriptor = ComponentDescriptor::sparse_blob(
1391            ComponentId::new(1),
1392            "health",
1393            ComponentSyncMode::Delta,
1394            ComponentMigrationMode::Copy,
1395            4,
1396        );
1397        let handle = EntityHandle::new(1, 0);
1398        let mut store = ComponentStore::default();
1399        store.reserve_component(descriptor.id, 64);
1400        store
1401            .set_blob_from_slice(&descriptor, handle, 1, &[1, 2, 3, 4])
1402            .expect("component write");
1403
1404        store.reclaim_retained_capacity();
1405
1406        assert_eq!(
1407            store.get_blob(descriptor.id, handle).expect("blob").bytes,
1408            [1, 2, 3, 4]
1409        );
1410        assert!(store.component_capacity(descriptor.id) >= 1);
1411    }
1412
1413    #[test]
1414    fn generated_schema_builds_descriptor_and_registers() {
1415        const FIELDS: &[ComponentFieldDescriptor] = &[
1416            ComponentFieldDescriptor::new("position", ComponentFieldType::Vec3, 0),
1417            ComponentFieldDescriptor::new("health", ComponentFieldType::U32, 12),
1418        ];
1419        let generated = GeneratedComponentSchema::new(
1420            ComponentId::new(8),
1421            "unit_state",
1422            ComponentStorageKind::SparseBlob,
1423            ComponentSyncMode::Delta,
1424            ComponentMigrationMode::Copy,
1425            16,
1426            FIELDS,
1427        );
1428
1429        generated.validate().expect("schema should be valid");
1430        assert_eq!(generated.fixed_size(), Some(16));
1431        assert_ne!(generated.schema_hash(), 0);
1432
1433        let descriptor = generated.descriptor();
1434        assert_eq!(descriptor.id, ComponentId::new(8));
1435        assert_eq!(descriptor.schema_hash, generated.schema_hash());
1436
1437        let mut registry = ComponentRegistry::default();
1438        let schema = registry
1439            .register_generated_schema(&generated)
1440            .expect("generated schema should register");
1441        assert_eq!(schema.fixed_size, Some(16));
1442        assert_eq!(
1443            registry
1444                .get(ComponentId::new(8))
1445                .expect("registered descriptor")
1446                .schema_hash,
1447            generated.schema_hash()
1448        );
1449    }
1450
1451    #[test]
1452    fn generated_schema_validation_rejects_bad_layouts() {
1453        const DUP_FIELDS: &[ComponentFieldDescriptor] = &[
1454            ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 0),
1455            ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 4),
1456        ];
1457        const OVERLAP_FIELDS: &[ComponentFieldDescriptor] = &[
1458            ComponentFieldDescriptor::new("left", ComponentFieldType::U32, 0),
1459            ComponentFieldDescriptor::new("right", ComponentFieldType::U32, 2),
1460        ];
1461        const OOB_FIELDS: &[ComponentFieldDescriptor] = &[ComponentFieldDescriptor::new(
1462            "wide",
1463            ComponentFieldType::U64,
1464            4,
1465        )];
1466
1467        let duplicate = GeneratedComponentSchema::new(
1468            ComponentId::new(1),
1469            "duplicate",
1470            ComponentStorageKind::SparseBlob,
1471            ComponentSyncMode::Delta,
1472            ComponentMigrationMode::Copy,
1473            8,
1474            DUP_FIELDS,
1475        );
1476        assert_eq!(
1477            duplicate.validate().expect_err("duplicate should fail"),
1478            ComponentSchemaError::DuplicateFieldName("x")
1479        );
1480
1481        let overlap = GeneratedComponentSchema::new(
1482            ComponentId::new(2),
1483            "overlap",
1484            ComponentStorageKind::SparseBlob,
1485            ComponentSyncMode::Delta,
1486            ComponentMigrationMode::Copy,
1487            8,
1488            OVERLAP_FIELDS,
1489        );
1490        assert_eq!(
1491            overlap.validate().expect_err("overlap should fail"),
1492            ComponentSchemaError::FieldOverlap {
1493                left: "left",
1494                right: "right"
1495            }
1496        );
1497
1498        let out_of_bounds = GeneratedComponentSchema::new(
1499            ComponentId::new(3),
1500            "oob",
1501            ComponentStorageKind::SparseBlob,
1502            ComponentSyncMode::Delta,
1503            ComponentMigrationMode::Copy,
1504            8,
1505            OOB_FIELDS,
1506        );
1507        assert_eq!(
1508            out_of_bounds
1509                .validate()
1510                .expect_err("out of bounds should fail"),
1511            ComponentSchemaError::FieldOutOfBounds {
1512                name: "wide",
1513                offset: 4,
1514                size: 8,
1515                max_bytes: 8
1516            }
1517        );
1518    }
1519}