Graph Data
Graphora starts with plain graph data and normalizes it into a GraphStore. The core package is framework-agnostic; it does not know about Canvas, React, DOM events, or layout algorithms.
import { createGraphStore } from '@graphora/core'
const store = createGraphStore({
nodes: [
{ id: 'api', label: 'API' },
{ id: 'db', label: 'Database' }
],
edges: [{ id: 'api-db', source: 'api', target: 'db' }]
})Raw Graph Rules
- Node IDs and explicit edge IDs can be strings or finite numbers.
- Internal lookup keys use
String(id), so1and"1"collide. edgesis optional and defaults to an empty array.- Edges default to
directed: true. - Edges without IDs receive deterministic generated IDs with the reserved
__edge:prefix. - Self-loops and parallel edges are allowed.
- Invalid graph loading throws
GraphErrorwith collectedGraphIssuevalues. - Optional labels, directed flags, positions, and recursive attributes are validated alongside IDs and endpoints.
JSON And Snapshots
RawGraph is also the save/reload boundary. parseRawGraphJson parses and validates JSON. serializeRawGraph validates input and refuses opaque data that JSON would lose or coerce, such as functions, bigint, dates, class instances, non-finite numbers, accessors, custom toJSON, and cycles.
Use snapshotToRawGraph(store.snapshot()) before saving runtime state. The conversion preserves every edge ID, including generated IDs, and copies raw structural containers while retaining opaque data references. Positioned nodes keep finite coordinates; nodes with a null runtime position omit the raw position field.
import {
createGraphStore,
parseRawGraphJson,
serializeRawGraph,
snapshotToRawGraph
} from '@graphora/core'
const saved = serializeRawGraph(snapshotToRawGraph(store.snapshot()), {
space: 2
})
const reloaded = createGraphStore(parseRawGraphJson(saved))Business Records
createRawGraphFromRecords is a one-call mapper rather than a second graph model. Supply node records and explicit accessors; add edge records with source/target accessors when needed. Accessors may return labels, data, attributes, positions, edge IDs, and directed flags. The mapper preserves returned data references, does not mutate the records, and validates the final RawGraph.
The data-roundtrip vanilla example maps service and dependency records, assigns generated edge IDs through normalization, saves JSON, and reloads it into a new engine.
Store Capabilities
GraphStore exposes deterministic read selectors such as getNode, getEdge, getNodes, getEdges, adjacency selectors, getBounds, getVersion, and snapshot.
Mutations are strict and versioned:
loadGraphclearaddNodeaddEdgeremoveNoderemoveEdgeupdateNodeDataupdateEdgeDataupdateNodeAttributesupdateEdgeAttributesupdateNodePosition
Successful mutations return { ok: true, version }, increment the version once, emit granular events, then emit one graph:changed event. Invalid mutations throw before state changes or events.
Collections And Atomic Updates
store.nodes(ids?) and store.edges(ids?) capture existing normalized IDs. selectNodes(predicate) and selectEdges(predicate) filter current members once. Collections resolve fresh records on each read, in store order; deleted members are skipped, and re-adding the same key restores them. New unrelated IDs do not join an existing collection.
const downstream = store.nodes(['api']).neighbors('out')
downstream.updateAttributes({ highlighted: true })
const directEdges = store.nodes(['api']).outgoingEdges()
console.log(downstream.ids(), directEdges.size)Collections support filtering, mapping and iteration. Node collections add incoming, outgoing and incident edges plus one-hop neighbors. Direction uses stored endpoints even for undirected edges; loops are excluded from neighbors but preserved by edge traversals. Recreate a filtered/traversed collection to reevaluate its membership after topology or data changes. These source-store helpers do not hide nodes in the rendered view. The highlighted attribute above is application metadata; use renderer style attributes to change visuals.
updateData and updateAttributes apply all active members in one applyBatch. For mixed node/edge operations, call store.applyBatch(operations) directly. Successful nonempty batches increment the version once and emit one graph:batch-applied summary followed by one graph:changed. Failed validation publishes nothing; empty batches are no-ops. Synchronous observer exceptions happen after publication and do not roll back committed state.
Data updaters must avoid store mutations: a version change during a callback aborts the helper with INVALID_GRAPH, keeping the callback's independent side effects. Returned arrays and records are immutable containers, while application data retains its original reference semantics.
Try the atomic batch and collection example to highlight direct downstream neighbors and compare successful/rejected batches.
Current Boundary
Core owns data normalization, graph identity, JSON interchange, record mapping, mutation semantics, atomic batches, runtime collections, snapshots, events, and bounds. Rendering, layouts, interactions, React bindings, schema migration, remote loading, non-JSON formats, undo/redo, visible-view filtering, grouping, and GraphML are outside the current core package.
Runnable examples live under examples/vanilla/ and use this data shape through createGraphEngine.