UNPKG

metagraph

Version:

A framework for building higher-order graph data structures

1,218 lines (1,203 loc) 41.1 kB
/*! * 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