Rule‑driven directed graph abstraction. Nodes carry domain data plus an immutable set of edge‑generation rules. Edges are materialized when nodes are inserted, removed, or when data is updated via updateNodeData().
⚠️ Beta Status: This library is currently in active beta. Expect breaking changes in any release without prior deprecation warnings until a stable 1.0.0 is published. Pin exact versions if you need stability.
yarn add @bedrock-core/network
# or
npm install @bedrock-core/networkimport { NetworkManager, Rule, RuleDirection } from '@bedrock-core/network';
interface Item { value: number }
const greaterThan: Rule<Item> = {
direction: RuleDirection.Outgoing, // initiates edges to smaller valued nodes when accepted
match: (s, t) => s.value > t.value,
};
const acceptSmaller: Rule<Item> = {
direction: RuleDirection.Incoming, // only accepts connections from higher valued nodes
match: (self, initiator) => initiator.value > self.value,
};
const mgr = new NetworkManager<Item>();
// Reuse the same rule object for multiple nodes (safe & common)
mgr.createNode('a', { value: 10 }, [greaterThan, acceptSmaller]);
mgr.createNode('b', { value: 5 }, [acceptSmaller]); // b can accept from higher but can't initiate upwards
mgr.createNode('c', { value: 20 }, [greaterThan, acceptSmaller]); // c can initiate to a & b (who accept),
// and a accepts c (a<-c) but b only accepts from higher so c->b edge forms; b has no outgoing to c.The library provides two approaches for updating node data:
-
updateNodeData(): Updates data and recalculates all edges for the node- Use when data changes should affect edge connectivity
- Slower but ensures edges reflect current data
-
Direct mutation: Modify
node.datadirectly viagetNode(id).data- Use for performance-critical scenarios when edges don't need updating
- Faster but edges remain unchanged
// Method 1: Recalculate edges
mgr.updateNodeData('nodeId', newData);
// Method 2: Direct mutation (no edge recalculation)
const node = mgr.getNode('nodeId')!;
node.data.someProperty = newValue;-
Rule
- Pure edge intent / acceptance predicate.
- Shape:
{ direction?: 'outgoing'|'incoming'|'both'; match; targetFilter? }. - Direction semantics:
- outgoing: rule may initiate edges to targets it matches (handshake requires target has an incoming/both rule that matches back).
- incoming: rule may accept edges initiated by others that match it.
- both (default): participates in both roles.
targetFilter(optional) is only applied on the initiating side before callingmatch.- Rules are reusable & treated as immutable post node creation.
-
Node
{ id, data, rules }.rulesarray captured at creation time; do not mutate (clone if composing).datacan be mutated directly for performance, or viaupdateNodeData()for edge recalculation.
-
Network
- Thin subclass of the underlying graph implementation.
-
NetworkManager
- On insertion of node N, for each existing node E performs two independent handshakes:
- N -> E: needs an initiating rule on N (outgoing|both) whose
targetFilter(if any) andmatch(N,E)succeed AND an accepting rule on E (incoming|both) whosematch(E,N)succeeds. - E -> N: same logic with roles swapped.
- N -> E: needs an initiating rule on N (outgoing|both) whose
- If only the forward handshake succeeds you get a one‑way edge N->E; if both succeed you have two directed edges.
- Removal deletes node + incident edges;
updateNodeData()recalculates edges for the updated node.
- On insertion of node N, for each existing node E performs two independent handshakes:
Event: create node N
- Outgoing: every rule in
N.rulestested against every previously present node. - Incoming: every existing node's rules tested with N as target.
- Complexity: O(R * N) per insertion (R = total rules considered).
Event: update node data via updateNodeData()
- Removes all edges touching the node
- Recalculates edges between the updated node and all other nodes
- Complexity: O(R * N) per update (same as insertion)
Event: direct data mutation
- No edge changes.
Event: remove node
- All edges touching the node are removed; no other edges are revisited.
To reflect data‑dependent rule outcomes after a direct data change, call updateNodeData() or remove and recreate the node.
You can (and should) define common rule instances once and supply them to many nodes:
const connectedIfEven: Rule<Item> = {
targetFilter: t => (t.value & 1) === 0,
match: (s, t) => (s.value & 1) === 0 // both even
};
mgr.createNode('n1', { value: 2 }, [connectedIfEven]);
mgr.createNode('n2', { value: 4 }, [connectedIfEven]); // edges formed hereMutating connectedIfEven after nodes are created is undefined behavior (do not).
Edges are created per direction via handshake (initiator rule + acceptor rule). Reverse direction is independent. This permits:
const wantsAll: Rule<Item> = { direction: RuleDirection.Outgoing, match: () => true };
const onlyAcceptHigh: Rule<Item> = { direction: RuleDirection.Incoming, match: (self, init) => init.value > self.value };
// Node X (value 5) only has incoming acceptance; Node Y (value 10) has outgoing initiation.
// Y->X edge forms (Y initiates, X accepts). X->Y does NOT (X cannot initiate).- Provide a
targetFilterwhen you can reject most candidates cheaply. - Keep
matchside‑effect free and allocation light (it may run many times per insertion). - Use direct data mutation when edge recalculation isn't needed for better performance.
- Use
updateNodeData()when data changes should affect connectivity. - If large batch insertion becomes a need, consider a future bulk API (not implemented yet).
- Rules are immutable post node creation.
- Edges change on node insertion/removal and when
updateNodeData()is called. - Direct data mutation does not trigger edge recalculation.
- No hidden caches; traversal is over the underlying graph data structure.
- Reverse edges only appear if a separate successful handshake for that direction occurs.
MIT