metagraph
Version:
A framework for building higher-order graph data structures
1,218 lines (1,203 loc) • 41.1 kB
JavaScript
/*!
* metagraph.js <%= conf.pkg.version %>
* http://gordonwoodhull.github.io/metagraph.js/
* Copyright 2019 AT&T Intellectual Property
*
* Licensed under the MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
/**
* The version of the metagraph library.
*/
const version = '0.0.7';
/**
* Converts a value to an array. If the value is already an array, returns it as-is.
* If the value is null/undefined, returns an empty array. Otherwise, wraps the value in an array.
*
* @param a - The value to convert to an array
* @returns An array containing the value(s)
*
* @example
* ```typescript
* as_array(5) // [5]
* as_array([1, 2, 3]) // [1, 2, 3]
* as_array(null) // []
* ```
*/
function as_array(a) {
return !a ? [] : (Array.isArray(a) ? a : [a]);
}
/**
* Converts an object or array to a standardized key-value array format.
* If input is an object, converts it to an array of {key, value} pairs.
* If input is already an array, returns it as-is.
*
* @param o - The object or array to convert
* @returns An array of key-value pairs
*
* @example
* ```typescript
* as_keyvalue({a: 1, b: 2}) // [{key: 'a', value: 1}, {key: 'b', value: 2}]
* as_keyvalue([{key: 'x', value: 10}]) // [{key: 'x', value: 10}]
* ```
*/
function as_keyvalue(o) {
return !o ? [] : (Array.isArray(o) ? o : Object.keys(o).map(key => ({
key,
value: o[key]
})));
}
/**
* Creates a map from an array of values using provided key and value transformation functions.
*
* @param vals - Array of values to transform into a map
* @param keyf - Function to extract the key from each value
* @param wrap - Function to transform each value
* @returns A record/map from keys to transformed values
*
* @example
* ```typescript
* build_map([{id: 'a', name: 'Alice'}], x => x.id, x => x.name)
* // Result: {a: 'Alice'}
* ```
*/
function build_map(vals, keyf, wrap) {
return vals.reduce((o, val) => {
o[keyf(val)] = wrap(val);
return o;
}, {});
}
function graph_options$2(opts) {
return Object.assign({
nodeKey: (kv) => kv.key,
edgeKey: (kv) => kv.key,
nodeValue: (kv) => kv.value,
edgeValue: (kv) => kv.value,
edgeSource: (kv) => kv.value.source,
edgeTarget: (kv) => kv.value.target
}, opts || {});
}
class NodeImpl {
constructor(n, options, graph_ref, outs_map, ins_map) {
this.n = n;
this.options = options;
this.graph_ref = graph_ref;
this.outs_map = outs_map;
this.ins_map = ins_map;
}
value() {
return this.options.nodeValue(this.n);
}
key() {
return this.options.nodeKey(this.n);
}
graph() {
return this.graph_ref();
}
outs() {
return this.outs_map()[this.options.nodeKey(this.n)] || [];
}
ins() {
return this.ins_map()[this.options.nodeKey(this.n)] || [];
}
}
class EdgeImpl {
constructor(e, options, graph_ref) {
this.e = e;
this.options = options;
this.graph_ref = graph_ref;
}
value() {
return this.options.edgeValue(this.e);
}
key() {
return this.options.edgeKey(this.e);
}
graph() {
return this.graph_ref();
}
source() {
return this.graph_ref().node(this.options.edgeSource(this.e));
}
target() {
return this.graph_ref().node(this.options.edgeTarget(this.e));
}
}
class GraphImpl {
constructor(nodesList, edgesList, options) {
this.nodesList = nodesList;
this.edgesList = edgesList;
this.options = options;
}
build_node_map() {
if (this._nodeMap)
return;
this._nodeMap = build_map(this.nodesList, this.options.nodeKey, (n) => new NodeImpl(n, this.options, () => this, () => this.get_outs_map(), () => this.get_ins_map()));
}
build_edge_map() {
if (this._edgeMap)
return;
this._edgeMap = build_map(this.edgesList, this.options.edgeKey, (e) => new EdgeImpl(e, this.options, () => this));
}
build_nodes_list() {
if (this._nodesList)
return;
this.build_node_map();
this._nodesList = this.nodesList.map(v => this.node(this.options.nodeKey(v)));
}
build_edges_list() {
if (this._edgesList)
return;
this.build_edge_map();
this._edgesList = this.edgesList.map(v => this.edge(this.options.edgeKey(v)));
}
build_directional_edge_lists(acc) {
this.build_edge_map();
return this.edgesList.reduce((o, v) => {
const key = acc(v);
const list = o[key] = o[key] || [];
list.push(this.edge(this.options.edgeKey(v)));
return o;
}, {});
}
build_outs_map() {
if (this._outsList)
return;
this._outsList = this.build_directional_edge_lists(this.options.edgeSource);
}
build_ins_map() {
if (this._insList)
return;
this._insList = this.build_directional_edge_lists(this.options.edgeTarget);
}
get_outs_map() {
this.build_outs_map();
return this._outsList;
}
get_ins_map() {
this.build_ins_map();
return this._insList;
}
node(key) {
this.build_node_map();
return this._nodeMap[key];
}
edge(key) {
this.build_edge_map();
return this._edgeMap[key];
}
nodes() {
this.build_nodes_list();
return this._nodesList;
}
edges() {
this.build_edges_list();
return this._edgesList;
}
}
/**
* Creates a graph data structure from nodes and edges with adjacency list representation.
* Supports lazy evaluation and custom accessor functions for flexible data formats.
*
* @param nodes - Array of node objects or key-value record of nodes
* @param edges - Array of edge objects or key-value record of edges
* @param opts - Optional configuration for accessing node/edge properties
* @returns A graph instance with nodes and edges
*
* @example
* ```typescript
* const g = graph(
* [{key: 'a'}, {key: 'b'}],
* [{key: 'e1', value: {source: 'a', target: 'b'}}]
* );
* console.log(g.node('a').outs().length); // 1
* ```
*/
function graph(nodes, edges, opts) {
const nodesList = as_keyvalue(nodes);
const edgesList = as_keyvalue(edges);
const options = graph_options$2(opts);
return new GraphImpl(nodesList, edgesList, options);
}
function incidence_options(opts) {
const gropts = graph_options$2(opts);
return Object.assign({
nodeIncidences: (n) => n && (n.edges || n.ins || n.outs) || [],
incidencesOutward: (n) => {
const v = gropts.nodeValue(n);
return !v || !!(v.edges || v.outs);
}
}, gropts);
}
/**
* Creates a graph from nodes with incidence information (edges defined within node data).
* Automatically generates edges based on node incidence properties like 'edges', 'outs', or 'ins'.
*
* @param nodes - Array of nodes containing incidence information
* @param opts - Optional configuration for accessing node properties and incidence direction
* @returns A graph instance with generated edges from incidence data
*
* @example
* ```typescript
* const g = graph_incidence([
* {key: 'a', value: {edges: ['b', 'c']}},
* {key: 'b'},
* {key: 'c'}
* ]);
* console.log(g.edges().map(e => e.key())); // ['a-b', 'a-c']
* ```
*/
function graph_incidence(nodes, opts) {
const nodesList = as_keyvalue(nodes);
const options = incidence_options(opts);
const edges = [];
function edge_value(outward, nk, ik) {
return outward ? {
source: nk,
target: ik
} : {
source: ik,
target: nk
};
}
function edge_key(outward, nk, ik) {
return outward ? nk + '-' + ik : ik + '-' + nk;
}
nodesList.forEach(n => {
const nk = options.nodeKey(n);
const outward = options.incidencesOutward(n);
as_array(options.nodeIncidences(options.nodeValue(n)))
.forEach(ik => {
edges.push({
key: edge_key(outward, nk, ik),
value: edge_value(outward, nk, ik)
});
});
});
return graph(nodesList, edges, opts);
}
/**
* Automatically detects the graph format and creates the appropriate graph instance.
* Supports both adjacency format (separate nodes/edges) and incidence format (edges in nodes).
*
* @param spec - Graph specification with either nodes/edges or incidences
* @param opts - Optional configuration for accessing node/edge properties
* @returns A graph instance created using the appropriate format
* @throws Error if the graph format is not recognized
*
* @example
* ```typescript
* // Adjacency format
* const g1 = graph_detect({
* nodes: [{key: 'a'}],
* edges: [{key: 'e', value: {source: 'a', target: 'b'}}]
* });
*
* // Incidence format
* const g2 = graph_detect({
* incidences: [{key: 'a', value: {edges: ['b']}}]
* });
* ```
*/
function graph_detect(spec, opts) {
if (spec.incidences) {
return graph_incidence(spec.incidences, opts);
}
else if (spec.nodes) {
return graph(spec.nodes, spec.edges, opts);
}
throw new Error('did not recognize graph format');
}
// Type guards for runtime validation
function isDataflowInstance(obj) {
return typeof obj === 'object' && obj !== null &&
'_yes_i_am_really_dataflow' in obj &&
obj._yes_i_am_really_dataflow === true;
}
class DataflowInstanceImpl {
constructor(flowgraph, instance, inputs) {
this.flowgraph = flowgraph;
this.instance = instance;
this.inputs = inputs;
this._yes_i_am_really_dataflow = true;
}
calc(id) {
if (!this.instance[id]) {
const n = this.flowgraph.node(id);
if (!n) {
throw new Error(`Node ${id} not found in dataflow graph`);
}
const nodeValue = n.value();
this.instance[id] = nodeValue.calc(this).apply(null, n.ins().map((e) => {
return this.calc(e.source().key());
}));
console.assert(this.instance[id]);
}
return this.instance[id];
}
input(namespace, field) {
const input = this.inputs[namespace];
if (input && isDataflowInstance(input)) {
return input.calc(field);
}
else if (input && typeof input === 'object') {
return input[field];
}
return undefined;
}
}
class DataflowImpl {
constructor(spec, options) {
this.flowgraph = graph_detect(spec, options);
}
instantiate(instance, inputs) {
return new DataflowInstanceImpl(this.flowgraph, instance, inputs);
}
}
/**
* Creates a dataflow computation graph for lazy evaluation of dependent calculations.
* Nodes represent computations and edges represent data dependencies.
*
* @param spec - Graph specification defining the computation nodes and their dependencies
* @param options - Optional configuration for graph format
* @returns A dataflow instance that can be instantiated with input data
*
* @example
* ```typescript
* const flow = dataflow({
* nodes: [
* {key: 'a'},
* {key: 'sum', calc: (flow) => (a, b) => a + b}
* ],
* edges: [
* {key: 'dep', value: {source: 'a', target: 'sum'}}
* ]
* });
* const instance = flow.instantiate({sum: 10}, {a: 5});
* console.log(instance.calc('sum')); // 15
* ```
*/
function dataflow(spec, options) {
return new DataflowImpl(spec, options);
}
/**
* Creates a reusable graph pattern that can generate graph instances from data.
* Patterns define the structure and behavior of graphs through dataflow and interface specifications.
*
* @param spec - Pattern specification containing dataflow computation and interface definition
* @param flowspecs - Optional additional flowspecs for composed patterns
* @returns A pattern graph that can create instances via the 'create' method
*
* @example
* ```typescript
* const graphPattern = pattern(graph_pattern());
* const instance = graphPattern.node('Graph').value().create({
* nodes: [{key: 'a'}, {key: 'b'}],
* edges: [{key: 'e', value: {source: 'a', target: 'b'}}]
* });
* ```
*/
const pattern = (spec, flowspecs) => {
const flowspec = spec.dataflow && graph_detect(spec.dataflow);
const interf = graph_detect(spec.interface);
const defn = { node: {}, edge: {} };
interf.nodes().forEach(inode => {
defn.node[inode.key()] = {
members: {},
class_members: {},
wrap: (flow, val) => {
const wrapper = {};
const members = defn.node[inode.key()].members;
Object.keys(members).forEach(name => {
wrapper[name] = members[name].defn(defn, flow, val);
});
return wrapper;
}
};
});
const resolve = (deps, funfun) => (defn, flow, val) => {
const action = funfun(defn, flow, val);
return (...args) => action.apply(null, deps.map(dep => {
const parts = dep.split('.');
if (parts.length > 1) {
return flow.input(parts[0], parts[1]);
}
else {
return flow.calc(dep);
}
})).apply(null, args);
};
interf.edges().forEach(iedge => {
const ekey = iedge.key();
const evalue = iedge.value();
const fs = flowspec || (flowspecs === null || flowspecs === void 0 ? void 0 : flowspecs[ekey.split('.')[0]]);
const action = evalue.member;
if (action && action.funfun) {
const funfun = action.funfun(fs, iedge, flowspecs);
const deps = as_array(evalue.deps);
const resolvedFunfun = resolve(deps, funfun);
defn.node[iedge.source().key()].members[evalue.name] = { defn: resolvedFunfun };
}
});
interf.nodes().forEach(inode => {
const nkey = inode.key();
inode.value();
const fs = flowspec || (flowspecs === null || flowspecs === void 0 ? void 0 : flowspecs[nkey.split('.')[0]]);
as_array(inode.value()).forEach((spec) => {
as_keyvalue(spec.class_members).forEach((cmemspec) => {
defn.node[nkey].class_members[cmemspec.key] = cmemspec.value(fs, inode);
});
as_keyvalue(spec.members).forEach((memspec) => {
const mem = memspec.value(fs, inode);
defn.node[nkey].members[memspec.key] = {
accessor: mem.accessor,
defn: mem.defn
};
});
});
});
const inodes2 = interf.nodes().map(n => {
const n2 = { key: n.key(), value: {} };
const class_members = defn.node[n.key()].class_members;
Object.keys(class_members).forEach(name => {
n2.value[name] = class_members[name].defn(defn);
});
return n2;
});
const iedges2 = interf.edges().map(e => ({
key: e.key(),
value: {
source: e.source().key(),
target: e.target().key()
}
}));
return graph_detect({ nodes: inodes2, edges: iedges2 });
};
const define_dataflow = (flowspec, defn) => {
const flownodes = flowspec.nodes().map(fsn => ({
key: fsn.key(),
value: {
calc: fsn.value().node.calc(fsn)(defn)
}
}));
return dataflow({
nodes: flownodes,
edges: flowspec.edges().map(e => ({ key: e.key(), value: e.value() }))
});
};
/**
* Performs a topological sort on a directed acyclic graph (DAG).
* Returns nodes in an order where all dependencies come before dependents.
*
* @param graph - The directed graph to sort (must be acyclic)
* @returns Array of nodes in topological order
* @throws Error if the graph contains cycles (is not a DAG)
*
* @example
* ```typescript
* const g = graph(
* [{key: 'a'}, {key: 'b'}, {key: 'c'}],
* [{key: 'e1', value: {source: 'a', target: 'b'}},
* {key: 'e2', value: {source: 'b', target: 'c'}}]
* );
* const sorted = topological_sort(g);
* console.log(sorted.map(n => n.key())); // ['a', 'b', 'c']
* ```
*/
function topological_sort(graph) {
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
const stacked = {};
const marked = {};
const sorted = [];
function visit(n) {
if (stacked[n.key()]) {
throw new Error('not a DAG');
}
if (!marked[n.key()]) {
stacked[n.key()] = true;
n.outs().forEach(e => {
visit(e.target());
});
marked[n.key()] = true;
stacked[n.key()] = false;
sorted.unshift(n);
}
}
let i = 0;
const nodes = graph.nodes();
while (Object.keys(marked).length < nodes.length) {
while (marked[nodes[i].key()])
++i;
visit(nodes[i]);
}
return sorted;
}
/**
* Composes multiple graph patterns into a single unified pattern.
* Resolves dependencies between patterns and creates a combined interface.
*
* @param composition - A graph where nodes are patterns and edges define dependencies
* @returns A composed pattern that combines all input patterns
*
* @example
* ```typescript
* const comp = compose(graph_detect({
* nodes: {
* graph: graph_pattern(),
* subgraph: subgraph_pattern()
* },
* edges: {
* dependency: {source: 'graph', target: 'subgraph', input: 'parent'}
* }
* }));
* ```
*/
const compose = (composition) => {
const sorted = topological_sort(composition);
const built = {};
const flowspecs = {};
const input_edge = (patnode, name) => patnode.ins().find((pe) => pe.value().input === name);
// resolve dependencies and build patterns
sorted.forEach(patnode => {
const flowspec = graph_detect(patnode.value().dataflow);
const fnodes = flowspec.nodes().map(fn => {
const v2 = Object.assign({}, fn.value());
v2.refs = as_array(v2.refs).map((ref) => {
const parts = ref.split('.');
if (parts.length > 1) {
const patedge = input_edge(patnode, parts[0]);
return patedge.source().key() + '.' + parts[1];
}
else
return patnode.key() + '.' + parts[0];
});
return {
key: fn.key(),
value: v2
};
});
const fedges = flowspec.edges().map(e => ({ key: e.key(), value: e.value() }));
flowspecs[patnode.key()] = graph_detect({ nodes: fnodes, edges: fedges });
const interf = patnode.value().interface;
built[patnode.key()] = graph_detect({
nodes: interf.nodes,
edges: interf.edges
});
});
// unite patterns
const nodes = [];
const edges = [];
const mappings = {};
const lookup = (key) => mappings[key] || key;
sorted.forEach(patnode => {
const pattern = built[patnode.key()];
pattern.nodes().forEach(inode => {
const key = patnode.key() + '.' + inode.key();
const ref = as_array(inode.value()).find((spec) => typeof spec === 'string');
if (ref) {
const parts = ref.split('.');
const patedge = input_edge(patnode, parts[0]);
const key2 = lookup(patedge.source().key() + '.' + parts[1]);
mappings[key] = key2;
}
else
nodes.push({
key: key,
value: inode.value()
});
});
pattern.edges().forEach(iedge => {
const val2 = Object.assign({}, iedge.value());
val2.source = lookup(patnode.key() + '.' + iedge.source().key());
val2.target = lookup(patnode.key() + '.' + iedge.target().key());
edges.push({
key: patnode.key() + '.' + iedge.key(),
value: val2
});
});
});
return pattern({
interface: {
nodes,
edges
}
}, flowspecs);
};
/**
* Creates an interface specification for objects that can create instances.
* Typically used for Graph patterns to enable the 'create' method.
*
* @param flowkey - The key in the environment where the created instance will be stored
* @returns An interface specification with a 'create' class member
*
* @example
* ```typescript
* // Used in graph patterns to enable graph.create(data)
* const graphInterface = createable('graph');
* ```
*/
const createable = (flowkey) => ({
class_members: {
create: (flowspec, inode) => ({
defn: (defn) => {
const flowg = define_dataflow(flowspec, defn);
return (data) => {
const env = {};
const flow = flowg.instantiate(env, { data });
env[flowkey] = defn.node[inode.key()].wrap(flow, data[inode.key()]);
return env[flowkey];
};
}
})
}
});
/**
* Creates an interface specification for a method that calls a function on the object's value.
* Commonly used to create 'key' and 'value' accessor methods.
*
* @param methodname - The name of the method to create
* @returns A function that takes an accessor function and returns an interface specification
*
* @example
* ```typescript
* // Create key() and value() methods
* const keyMethod = call('key')(obj => obj.key);
* const valueMethod = call('value')(obj => obj.value);
* ```
*/
const call = (methodname) => (f) => ({
members: [{
key: methodname,
value: (flowspec, inode) => ({
accessor: f,
defn: (defn, flow, val) => () => f(val)
})
}]
});
/**
* Creates a reference to another interface node.
* Used for connecting interface elements together.
*
* @param inode - The interface node to reference
* @returns A reference specification
*/
const reference = (inode) => ({
reference: inode
});
/**
* Creates an interface method that fetches (returns) its input unchanged.
* Acts as a pass-through for data flow.
*
* @returns An interface specification for fetching data
*/
const fetch = () => ({
funfun: (flowspec, iedge) => (defn, flow) => (x) => () => x
});
/**
* Creates an interface method that looks up values in a map using function arguments.
* The lookup key is derived by applying an optional accessor function to the argument.
*
* @param access - Optional function to transform the lookup key (defaults to identity)
* @returns An interface specification for argument-based map lookup
*
* @example
* ```typescript
* // Direct lookup: map[key]
* const directLookup = lookupArg();
*
* // Transform key before lookup: map[key.id]
* const idLookup = lookupArg(key => key.id);
* ```
*/
const lookupArg = (access) => ({
funfun: (flowspec, iedge) => {
const accessor = access || ((x) => x);
return (defn, flow, val) => (map) => (key) => map[accessor(key)];
}
});
/**
* Creates an interface method that looks up values in a map using the object's own value.
* The lookup key is derived by applying an accessor function to the object's value.
*
* @param access - Function to extract the lookup key from the object's value
* @returns An interface specification for value-based map lookup
*
* @example
* ```typescript
* // Lookup using edge.source: map[edge.value.source]
* const sourceLookup = lookupFVal(edge => edge.source);
* ```
*/
const lookupFVal = (access) => ({
funfun: (flowspec, iedge) => (defn, flow, val) => (map) => () => map[access(val)]
});
/**
* Creates an interface method that looks up values using the source node's key accessor.
* Commonly used for accessing lists associated with specific keys (like adjacency lists).
*
* @returns An interface specification for key-based map lookup with fallback to empty array
*
* @example
* ```typescript
* // Get outgoing edges for a node: outEdges[node.key()] || []
* const nodeOuts = lookupKVal();
* ```
*/
const lookupKVal = () => ({
funfun: (flowspec, iedge) => (defn, flow, val) => (map) => () => map[defn.node[iedge.source().key()].members.key.accessor(val)] || []
});
/**
* Creates an interface method for generating subgraphs from a parent graph.
* Supports complex dataflow with multiple pattern compositions and data filtering.
*
* @returns An interface specification for subgraph creation
*
* @example
* ```typescript
* // Used in subgraph patterns to enable parent.subgraph(nodeKeys, edgeKeys, data)
* const subgraphMethod = subgraph();
* ```
*/
const subgraph = () => ({
funfun: (flowspec, iedge, flowspecs) => (defn, flow, val) => {
const subflow = define_dataflow(flowspec, defn);
let graflow = subflow;
const parts = iedge.target().key().split('.');
if (parts.length > 1) {
const dest = parts[0];
graflow = define_dataflow(flowspecs[dest], defn);
}
return () => (nodeKeys, edgeKeys, gdata) => {
const sgflow = subflow.instantiate({}, {
data: {
nodeKeys,
edgeKeys
},
parent: flow
});
const genv = {};
const gflow = graflow.instantiate(genv, {
data: sgflow
});
genv.graph = defn.node[iedge.target().key()].wrap(gflow, gdata);
return genv.graph;
};
}
});
/**
* Creates a dataflow input node that reads data from the input namespace.
*
* @param path - Optional path to the input data (defaults to node key)
* @returns A dataflow node specification for reading input data
*
* @example
* ```typescript
* // Read from 'data.nodeList'
* const nodeInput = input('data.nodeList');
*
* // Read from default path
* const defaultInput = input();
* ```
*/
const input = (path) => ({
calc: (fnode) => {
const actualPath = path || fnode.key();
const parts = actualPath.split('.');
const [namespace, name] = parts.length > 1 ? parts : ['data', actualPath];
return (defn) => (flow) => () => flow.input(namespace, name);
}
});
/**
* Creates a dataflow output node that passes through its input unchanged.
* Used as a terminal node in dataflow graphs.
*
* @param name - Optional output name (unused in current implementation)
* @param namespace - Optional output namespace (unused in current implementation)
* @returns A dataflow node specification that passes through input data
*/
const output = (name, namespace) => ({
calc: (fnode) => (defn) => (flow) => (x) => x
});
/**
* Creates a dataflow node that builds a map from array data using pattern-defined accessors.
* Maps each item in the input array to a wrapped object using the pattern's key accessor.
*
* @returns A dataflow node specification for creating maps from arrays
*
* @example
* ```typescript
* // In a pattern definition
* const mapNode = map(); // Creates {key1: wrappedItem1, key2: wrappedItem2, ...}
* ```
*/
const map = () => ({
calc: (fnode) => {
const iref = as_array(fnode.value().refs)[0];
return (defn) => (flow) => (data) => build_map(data, defn.node[iref].members.key.accessor, defn.node[iref].wrap.bind(null, flow));
}
});
/**
* Creates a dataflow node representing a singleton that must be initialized elsewhere.
* Throws an error if accessed before initialization.
*
* @returns A dataflow node specification for singleton values
* @throws Error when accessed before proper initialization
*/
const singleton = () => ({
calc: (fnode) => (defn) => (flow) => () => {
throw new Error('singleton not initialized');
}
});
/**
* Creates a dataflow node that converts a map back to a list using array order.
* Takes input data array and map, returns array of mapped objects in original order.
*
* @returns A dataflow node specification for creating ordered lists from maps
*
* @example
* ```typescript
* // Convert map back to ordered array
* const listNode = list(); // [data[0] -> map[key0], data[1] -> map[key1], ...]
* ```
*/
const list = () => ({
calc: (fnode) => {
const iref = as_array(fnode.value().refs)[0];
return (defn) => (flow) => (data, map) => data.map(val => map[defn.node[iref].members.key.accessor(val)]);
}
});
/**
* Creates a dataflow node that groups array items into lists based on a grouping function.
* Useful for creating adjacency lists or grouping related items.
*
* @param accessor - Function to extract the grouping key from each item
* @returns A dataflow node specification for grouping items into lists
*
* @example
* ```typescript
* // Group edges by source node
* const outEdges = map_of_lists(edge => edge.source);
* // Result: {nodeA: [edge1, edge2], nodeB: [edge3], ...}
* ```
*/
const map_of_lists = (accessor) => ({
calc: (fnode) => (defn) => (flow) => (data, map) => {
const iref = as_array(fnode.value().refs)[0];
return data.reduce((o, v) => {
const key = accessor(v);
const list = o[key] = o[key] || [];
list.push(map[defn.node[iref].members.key.accessor(v)]);
return o;
}, {});
}
});
/**
* Creates a dataflow node that filters array items to include only those with keys in a specified set.
* Used for creating subgraphs or filtering collections.
*
* @returns A dataflow node specification for filtering arrays by key membership
*
* @example
* ```typescript
* // Keep only nodes with keys in the specified set
* const filteredNodes = subset(); // filters items where item.key ∈ keySet
* ```
*/
const subset = () => ({
calc: (fnode) => {
const iref = as_array(fnode.value().refs)[0];
return (defn) => (flow) => (items, keys) => {
const keySet = new Set(keys);
return items.filter(r => keySet.has(defn.node[iref].members.key.accessor(r)));
};
}
});
const graph_options$1 = (opts) => Object.assign({
nodeKey: (kv) => kv.key,
edgeKey: (kv) => kv.key,
nodeValue: (kv) => kv.value,
edgeValue: (kv) => kv.value,
edgeSource: (kv) => kv.value.source,
edgeTarget: (kv) => kv.value.target
}, opts || {});
/**
* Creates a standard graph pattern specification for building graph instances.
* Defines the dataflow computation and interface for creating graph objects with nodes and edges.
*
* @param opts - Optional graph options for customizing node/edge accessors
* @returns A pattern specification that can create graph instances
*
* @example
* ```typescript
* // Create a graph pattern and use it to build a graph
* const pat = pattern(graph_pattern());
* const myGraph = pat.node('Graph').value().create({
* nodes: [{key: 'a'}, {key: 'b'}],
* edges: [{key: 'edge', value: {source: 'a', target: 'b'}}]
* });
* ```
*/
const graph_pattern = (opts) => {
const options = graph_options$1(opts);
return {
dataflow: {
incidences: {
nodes: { node: input() },
edges: { node: input() },
node_by_key: {
node: map(),
refs: 'Node',
ins: 'nodes'
},
edge_by_key: {
node: map(),
refs: 'Edge',
ins: 'edges'
},
graph: { node: singleton() },
node_list: {
node: list(),
refs: 'Node',
ins: ['nodes', 'node_by_key']
},
edge_list: {
node: list(),
refs: 'Edge',
ins: ['edges', 'edge_by_key']
},
node_outs: {
node: map_of_lists(options.edgeSource),
refs: 'Node',
ins: ['edges', 'edge_by_key']
},
node_ins: {
node: map_of_lists(options.edgeTarget),
refs: 'Node',
ins: ['edges', 'edge_by_key']
}
}
},
interface: {
nodes: {
Graph: createable('graph'),
Node: [call('key')(options.nodeKey), call('value')(options.nodeValue)],
Edge: [call('key')(options.edgeKey), call('value')(options.edgeValue)]
},
edges: {
graph_node: {
name: 'node',
source: 'Graph', target: 'Node',
deps: 'node_by_key',
member: lookupArg()
},
node_graph: {
name: 'graph',
source: 'Node', target: 'Graph',
deps: 'graph',
member: fetch()
},
graph_nodes: {
name: 'nodes',
source: 'Graph', target: 'Node',
deps: 'node_list',
member: fetch()
},
graph_edge: {
name: 'edge',
source: 'Graph', target: 'Edge',
deps: 'edge_by_key',
member: lookupArg()
},
edge_graph: {
name: 'graph',
source: 'Edge', target: 'Graph',
deps: 'graph',
member: fetch()
},
graph_edges: {
name: 'edges',
source: 'Graph', target: 'Edge',
deps: 'edge_list',
member: fetch()
},
edge_source: {
name: 'source',
source: 'Edge', target: 'Node',
deps: 'node_by_key',
member: lookupFVal(options.edgeSource)
},
edge_target: {
name: 'target',
source: 'Edge', target: 'Node',
deps: 'node_by_key',
member: lookupFVal(options.edgeTarget)
},
node_outs: {
name: 'outs',
source: 'Node', target: 'Edge',
deps: 'node_outs',
member: lookupKVal()
},
node_ins: {
name: 'ins',
source: 'Node', target: 'Edge',
deps: 'node_ins',
member: lookupKVal()
}
}
}
};
};
const graph_options = (opts) => Object.assign({
nodeKey: (kv) => kv.key,
edgeKey: (kv) => kv.key,
nodeValue: (kv) => kv.value,
edgeValue: (kv) => kv.value,
edgeSource: (kv) => kv.value.source,
edgeTarget: (kv) => kv.value.target
}, opts || {});
/**
* Creates a subgraph pattern specification for filtering parent graphs.
* Enables creation of subgraphs by selecting subsets of nodes and edges from parent graphs.
*
* @param opts - Optional graph options for customizing node/edge accessors
* @returns A pattern specification for creating subgraph functionality
*
* @example
* ```typescript
* // Used in composed patterns to enable subgraph operations
* const subPat = subgraph_pattern();
* // Results in parent.subgraph(['nodeA', 'nodeB'], ['edge1'], data)
* ```
*/
const subgraph_pattern = (opts) => {
graph_options(opts);
return {
dataflow: {
incidences: {
parent_nodes: { node: input('parent.nodes') },
parent_edges: { node: input('parent.edges') },
node_keys: { node: input('nodeKeys') },
edge_keys: { node: input('edgeKeys') },
subset_nodes: {
node: subset(),
refs: 'child.Node',
ins: ['parent_nodes', 'node_keys']
},
subset_edges: {
node: subset(),
refs: 'child.Edge',
ins: ['parent_edges', 'edge_keys']
},
nodes: {
node: output(),
ins: 'subset_nodes'
},
edges: {
node: output(),
ins: 'subset_edges'
}
}
},
interface: {
nodes: {
ParentGraph: 'parent.Graph',
ChildGraph: 'child.Graph'
},
edges: {
subgraph: {
name: 'subgraph',
source: 'ParentGraph', target: 'ChildGraph',
member: subgraph()
},
subnode: {
name: 'subnode',
source: 'ParentGraph', target: 'ChildGraph',
deps: 'parent.node_by_key',
member: lookupArg()
},
subedge: {
name: 'subedge',
source: 'ParentGraph', target: 'ChildGraph',
deps: 'parent.edge_by_key',
flow: lookupArg()
},
subgraphS: {
name: 'subgraph',
source: 'ChildGraph', target: 'ParentGraph',
member: subgraph()
},
subnodeS: {
name: 'subnode',
source: 'ChildGraph', target: 'ParentGraph',
deps: 'node_by_key', // should be child:
member: lookupArg((x) => x.key())
},
subedgeS: {
name: 'subedge',
source: 'ChildGraph', target: 'ParentGraph',
deps: 'edge_by_key', // should be child:
flow: lookupArg((x) => x.key())
}
}
}
};
};
/**
* # Metagraph.js
*
* A TypeScript library for building higher-order graph data structures with composable patterns,
* lazy evaluation, and dataflow computation capabilities.
*
* ## Core Features
*
* - **Graph Data Structures**: Adjacency and incidence list representations
* - **Lazy Evaluation**: On-demand computation of graph properties
* - **Dataflow Graphs**: Computation graphs with dependency resolution
* - **Graph Patterns**: Reusable templates for creating graph instances
* - **Pattern Composition**: Combine multiple patterns into complex structures
* - **Topological Sorting**: DAG ordering algorithms
*
* ## Basic Usage
*
* ```typescript
* import { graph, pattern, graph_pattern } from 'metagraph';
*
* // Create a simple graph
* const g = graph(
* [{key: 'a'}, {key: 'b'}],
* [{key: 'edge', value: {source: 'a', target: 'b'}}]
* );
*
* // Create a graph using patterns
* const graphPat = pattern(graph_pattern());
* const instance = graphPat.node('Graph').value().create({
* nodes: [{key: 'x'}, {key: 'y'}],
* edges: [{key: 'connection', value: {source: 'x', target: 'y'}}]
* });
* ```
*
* @packageDocumentation
*/
// Re-export everything from individual modules
const key = call('key');
const value = call('value');
export { call, compose, createable, dataflow, fetch, graph, graph as graph_adjacency, graph_detect, graph_incidence, graph_pattern, input, key, list, lookupArg, lookupFVal, lookupKVal, map, map_of_lists, output, pattern, reference, singleton, subgraph, subgraph_pattern, subset, topological_sort, value, version };
//# sourceMappingURL=metagraph.esm.js.map