Skip to content

Commit ff1f915

Browse files
committed
feat(entity): add relation ID decoding and validation functions
1 parent a01bf5a commit ff1f915

File tree

1 file changed

+73
-87
lines changed

1 file changed

+73
-87
lines changed

src/entity.ts

Lines changed: 73 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ export const ENTITY_ID_START = 1024;
4141
export const RELATION_SHIFT = 2 ** 42;
4242
export const WILDCARD_TARGET_ID = 0;
4343

44+
/**
45+
* Internal function to decode a relation ID into raw component and target IDs
46+
* @param id The EntityId to decode
47+
* @returns Object with componentId and targetId, or null if not a relation
48+
*/
49+
function decodeRelationRaw(id: EntityId<any>): { componentId: number; targetId: number } | null {
50+
if (id >= 0) return null;
51+
const absId = -id;
52+
const componentId = Math.floor(absId / RELATION_SHIFT);
53+
const targetId = absId % RELATION_SHIFT;
54+
return { componentId, targetId };
55+
}
56+
57+
/**
58+
* Check if a component ID is valid (1-1023)
59+
*/
60+
function isValidComponentId(componentId: number): boolean {
61+
return componentId >= 1 && componentId <= COMPONENT_ID_MAX;
62+
}
63+
4464
/**
4565
* Create a component ID
4666
* @param id Component identifier (1-1023)
@@ -128,12 +148,8 @@ export function isRelationId<T>(id: EntityId<T>): id is RelationId<T> {
128148
* Check if an ID is a wildcard relation id
129149
*/
130150
export function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T> {
131-
if (!isRelationId(id)) {
132-
return false;
133-
}
134-
const absId = -id;
135-
const targetId = absId % RELATION_SHIFT;
136-
return targetId === WILDCARD_TARGET_ID;
151+
const decoded = decodeRelationRaw(id);
152+
return decoded !== null && decoded.targetId === WILDCARD_TARGET_ID;
137153
}
138154

139155
/**
@@ -146,13 +162,18 @@ export function decodeRelationId(relationId: RelationId<any>): {
146162
targetId: EntityId<any>;
147163
type: "entity" | "component" | "wildcard";
148164
} {
149-
if (!isRelationId(relationId)) {
165+
const decoded = decodeRelationRaw(relationId);
166+
if (decoded === null) {
150167
throw new Error("ID is not a relation ID");
151168
}
152-
const absId = -relationId;
153169

154-
const componentId = Math.floor(absId / RELATION_SHIFT) as ComponentId<any>;
155-
const targetId = (absId % RELATION_SHIFT) as EntityId<any>;
170+
const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
171+
if (!isValidComponentId(rawComponentId)) {
172+
throw new Error("Invalid component ID in relation");
173+
}
174+
175+
const componentId = rawComponentId as ComponentId<any>;
176+
const targetId = rawTargetId as EntityId<any>;
156177

157178
// Determine type based on targetId range
158179
if (targetId === WILDCARD_TARGET_ID) {
@@ -178,11 +199,8 @@ export function getIdType(
178199
if (isRelationId(id)) {
179200
try {
180201
const decoded = decodeRelationId(id);
181-
// Validate that componentId and targetId are valid
182-
if (
183-
!isComponentId(decoded.componentId) ||
184-
(decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId))
185-
) {
202+
// Validate that componentId and targetId are valid (decodeRelationId already checks componentId)
203+
if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) {
186204
return "invalid";
187205
}
188206
switch (decoded.type) {
@@ -233,11 +251,8 @@ export function getDetailedIdType(id: EntityId<any>):
233251
if (isRelationId(id)) {
234252
try {
235253
const decoded = decodeRelationId(id);
236-
// Validate that componentId and targetId are valid
237-
if (
238-
!isComponentId(decoded.componentId) ||
239-
(decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId))
240-
) {
254+
// Validate that targetId is valid (decodeRelationId already checks componentId)
255+
if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) {
241256
return { type: "invalid" };
242257
}
243258
let type: "entity-relation" | "component-relation" | "wildcard-relation";
@@ -290,11 +305,8 @@ export function inspectEntityId(id: EntityId<any>): string {
290305
if (isRelationId(id)) {
291306
try {
292307
const decoded = decodeRelationId(id);
293-
// Validate that both component and target IDs are valid
294-
if (
295-
!isComponentId(decoded.componentId) ||
296-
(decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId))
297-
) {
308+
// Validate that targetId is valid (decodeRelationId already checks componentId)
309+
if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) {
298310
return `Invalid Relation ID (${id})`;
299311
}
300312
const componentStr = `Component ID (${decoded.componentId})`;
@@ -570,27 +582,32 @@ export function isDontFragmentComponent(id: ComponentId<any>): boolean {
570582
return dontFragmentFlags.has(id);
571583
}
572584

585+
/**
586+
* Generic function to check relation flags with specific target conditions
587+
* @param id The entity/relation ID to check
588+
* @param flagBitSet The bitset for the flag
589+
* @param targetCondition Function to check target ID condition
590+
* @returns true if the condition is met, false otherwise
591+
*/
592+
function checkRelationFlag(
593+
id: EntityId<any>,
594+
flagBitSet: BitSet,
595+
targetCondition: (targetId: number) => boolean,
596+
): boolean {
597+
const decoded = decodeRelationRaw(id);
598+
if (decoded === null) return false;
599+
const { componentId, targetId } = decoded;
600+
return isValidComponentId(componentId) && targetCondition(targetId) && flagBitSet.has(componentId);
601+
}
602+
573603
/**
574604
* Check if a relation ID is a dontFragment relation (entity-relation or component-relation with dontFragment component)
575605
* This is an optimized function that avoids the overhead of getDetailedIdType
576606
* @param id The entity/relation ID to check
577607
* @returns true if this is a dontFragment relation, false otherwise
578608
*/
579609
export function isDontFragmentRelation(id: EntityId<any>): boolean {
580-
// Only relation IDs are negative; non-relations return false immediately
581-
if (id >= 0) return false;
582-
583-
// Extract componentId directly from the relation encoding: -(componentId * RELATION_SHIFT + targetId)
584-
// componentId = floor(absId / RELATION_SHIFT)
585-
const absId = -id;
586-
const componentId = Math.floor(absId / RELATION_SHIFT);
587-
588-
// Wildcard relations (targetId === 0) are not considered dontFragment relations for this check
589-
const targetId = absId % RELATION_SHIFT;
590-
if (targetId === WILDCARD_TARGET_ID) return false;
591-
592-
// Check if componentId is valid and has dontFragment flag
593-
return componentId >= 1 && componentId <= COMPONENT_ID_MAX && dontFragmentFlags.has(componentId);
610+
return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
594611
}
595612

596613
/**
@@ -600,17 +617,7 @@ export function isDontFragmentRelation(id: EntityId<any>): boolean {
600617
* @returns true if this is a wildcard relation with dontFragment component, false otherwise
601618
*/
602619
export function isDontFragmentWildcard(id: EntityId<any>): boolean {
603-
// Only relation IDs are negative
604-
if (id >= 0) return false;
605-
606-
const absId = -id;
607-
const targetId = absId % RELATION_SHIFT;
608-
609-
// Must be a wildcard relation (targetId === 0)
610-
if (targetId !== WILDCARD_TARGET_ID) return false;
611-
612-
const componentId = Math.floor(absId / RELATION_SHIFT);
613-
return componentId >= 1 && componentId <= COMPONENT_ID_MAX && dontFragmentFlags.has(componentId);
620+
return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
614621
}
615622

616623
/**
@@ -620,24 +627,14 @@ export function isDontFragmentWildcard(id: EntityId<any>): boolean {
620627
* @returns true if this is an exclusive relation, false otherwise
621628
*/
622629
export function isExclusiveRelation(id: EntityId<any>): boolean {
623-
if (id >= 0) return false;
624-
const absId = -id;
625-
const targetId = absId % RELATION_SHIFT;
626-
if (targetId === WILDCARD_TARGET_ID) return false; // not an exclusive-specific relation
627-
const componentId = Math.floor(absId / RELATION_SHIFT);
628-
return componentId >= 1 && componentId <= COMPONENT_ID_MAX && exclusiveFlags.has(componentId);
630+
return checkRelationFlag(id, exclusiveFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
629631
}
630632

631633
/**
632634
* Check if a relation ID is a wildcard relation with exclusive component
633635
*/
634636
export function isExclusiveWildcard(id: EntityId<any>): boolean {
635-
if (id >= 0) return false;
636-
const absId = -id;
637-
const targetId = absId % RELATION_SHIFT;
638-
if (targetId !== WILDCARD_TARGET_ID) return false;
639-
const componentId = Math.floor(absId / RELATION_SHIFT);
640-
return componentId >= 1 && componentId <= COMPONENT_ID_MAX && exclusiveFlags.has(componentId);
637+
return checkRelationFlag(id, exclusiveFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
641638
}
642639

643640
/**
@@ -648,57 +645,46 @@ export function isExclusiveWildcard(id: EntityId<any>): boolean {
648645
* @returns true if this is an entity-relation with cascade delete, false otherwise
649646
*/
650647
export function isCascadeDeleteRelation(id: EntityId<any>): boolean {
651-
if (id >= 0) return false;
652-
const absId = -id;
653-
const targetId = absId % RELATION_SHIFT;
654-
// Wildcard relations (targetId === 0) don't cascade
655-
if (targetId === WILDCARD_TARGET_ID) return false;
656-
// Only entity-relations cascade (targetId >= ENTITY_ID_START)
657-
if (targetId < ENTITY_ID_START) return false;
658-
const componentId = Math.floor(absId / RELATION_SHIFT);
659-
return componentId >= 1 && componentId <= COMPONENT_ID_MAX && cascadeDeleteFlags.has(componentId);
648+
return checkRelationFlag(
649+
id,
650+
cascadeDeleteFlags,
651+
(targetId) => targetId !== WILDCARD_TARGET_ID && targetId >= ENTITY_ID_START,
652+
);
660653
}
661654

662655
/**
663656
* Get the componentId from a relation ID without fully decoding the relation.
664657
* Returns undefined for non-relation IDs or invalid component IDs.
665658
*/
666659
export function getComponentIdFromRelationId<T>(id: EntityId<T>): ComponentId<T> | undefined {
667-
if (id >= 0) return undefined;
668-
const absId = -id;
669-
const componentId = Math.floor(absId / RELATION_SHIFT);
670-
if (componentId < 1 || componentId > COMPONENT_ID_MAX) return undefined;
671-
return componentId as ComponentId<T>;
660+
const decoded = decodeRelationRaw(id);
661+
if (decoded === null || !isValidComponentId(decoded.componentId)) return undefined;
662+
return decoded.componentId as ComponentId<T>;
672663
}
673664

674665
/**
675666
* Get the targetId from a relation ID without fully decoding the relation.
676667
* Returns undefined for non-relation IDs.
677668
*/
678669
export function getTargetIdFromRelationId(id: EntityId<any>): EntityId<any> | undefined {
679-
if (id >= 0) return undefined;
680-
const absId = -id;
681-
return (absId % RELATION_SHIFT) as EntityId<any>;
670+
const decoded = decodeRelationRaw(id);
671+
return decoded?.targetId as EntityId<any>;
682672
}
683673

684674
/**
685675
* Check if an ID is an entity-relation (relation targeting an entity, not a component or wildcard)
686676
*/
687677
export function isEntityRelation(id: EntityId<any>): boolean {
688-
if (id >= 0) return false;
689-
const absId = -id;
690-
const targetId = absId % RELATION_SHIFT;
691-
return targetId >= ENTITY_ID_START;
678+
const decoded = decodeRelationRaw(id);
679+
return decoded !== null && decoded.targetId >= ENTITY_ID_START;
692680
}
693681

694682
/**
695683
* Check if an ID is a component-relation (relation targeting a component)
696684
*/
697685
export function isComponentRelation(id: EntityId<any>): boolean {
698-
if (id >= 0) return false;
699-
const absId = -id;
700-
const targetId = absId % RELATION_SHIFT;
701-
return targetId >= 1 && targetId <= COMPONENT_ID_MAX;
686+
const decoded = decodeRelationRaw(id);
687+
return decoded !== null && isValidComponentId(decoded.targetId);
702688
}
703689

704690
/**

0 commit comments

Comments
 (0)