diff --git a/packages/core/build/dox/hooks.js b/packages/core/build/dox/hooks.js index f727b29..45efd91 100644 --- a/packages/core/build/dox/hooks.js +++ b/packages/core/build/dox/hooks.js @@ -114,6 +114,21 @@ export const hooks = { // Do something with Class... }, + /** + * Invoked when flecks is building a fleck dependency graph. + * @param {Digraph} graph The dependency graph. + * @param {string} hook The hook; e.g. `@flecks/db/server`. + */ + '@flecks/core.priority': (graph, hook) => { + // Make `@flecks/user/server`'s `@flecks/server.up` implementation depend on + // `@flecks/db/server`'s: + if ('@flecks/server.up' === hook) { + graph.addDependency('@flecks/user/server', '@flecks/db/server'); + // Remove a dependency. + graph.removeDependency('@flecks/user/server', '@flecks/db/server'); + } + }, + /** * Invoked when the application is starting. Use for order-independent initialization tasks. */ diff --git a/packages/core/package.json b/packages/core/package.json index 306028b..69b0c0d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -81,7 +81,6 @@ "lodash.get": "^4.4.2", "lodash.intersectionby": "4.7.0", "lodash.set": "^4.3.2", - "lodash.without": "^4.4.0", "pirates": "^4.0.5", "rimraf": "^3.0.2", "source-map-loader": "4.0.1", diff --git a/packages/core/src/digraph.js b/packages/core/src/digraph.js index 25e1796..ed3f1da 100644 --- a/packages/core/src/digraph.js +++ b/packages/core/src/digraph.js @@ -6,6 +6,11 @@ class Digraph { this.arcs.get(tail).add(head); } + addDependency(head, tail) { + this.ensureTail(head); + this.ensureTail(tail).add(head); + } + detectCycles() { const cycles = []; const visited = new Set(); @@ -38,6 +43,7 @@ class Digraph { if (!this.arcs.has(tail)) { this.arcs.set(tail, new Set()); } + return this.arcs.get(tail); } neighbors(vertex) { @@ -73,6 +79,12 @@ class Digraph { .map(([vertex]) => vertex); } + removeDependency(head, tail) { + if (this.arcs.has(tail)) { + this.arcs.get(tail).delete(head); + } + } + get tails() { return this.arcs.keys(); } diff --git a/packages/core/src/flecks.js b/packages/core/src/flecks.js index 666b07f..84c01a3 100644 --- a/packages/core/src/flecks.js +++ b/packages/core/src/flecks.js @@ -8,7 +8,6 @@ import { import get from 'lodash.get'; import set from 'lodash.set'; -import without from 'lodash.without'; import D from './debug'; import Digraph from './digraph'; @@ -18,7 +17,7 @@ const debug = D('@flecks/core/flecks'); const debugSilly = debug.extend('silly'); // Symbol for hook ordering. -const HookOrder = Symbol.for('@flecks/core.hookOrder'); +const HookPriority = Symbol.for('@flecks/core.hookPriority'); // Symbols for Gathered classes. export const ById = Symbol.for('@flecks/core.byId'); @@ -63,6 +62,8 @@ export default class Flecks { config = {}; + $$expandedFlecksCache = {}; + flecks = {}; hooks = {}; @@ -95,40 +96,6 @@ export default class Flecks { debugSilly('config: %O', this.config); } - /** - * Configure a hook implementation to run after another implementation. - * - * @param {string[]} after - * @param {function} implementation - */ - static after(after, implementation) { - if (!implementation[HookOrder]) { - implementation[HookOrder] = {}; - } - if (!implementation[HookOrder].after) { - implementation[HookOrder].after = []; - } - implementation[HookOrder].after.push(...after); - return implementation; - } - - /** - * Configure a hook implementation to run before another implementation. - * - * @param {string[]} before - * @param {function} implementation - */ - static before(before, implementation) { - if (!implementation[HookOrder]) { - implementation[HookOrder] = {}; - } - if (!implementation[HookOrder].before) { - implementation[HookOrder].before = []; - } - implementation[HookOrder].before.push(...before); - return implementation; - } - /** * Configure defaults for a fleck. * @@ -233,13 +200,15 @@ export default class Flecks { * @returns {string[]} The expanded list of flecks. */ expandedFlecks(hook) { + if (this.$$expandedFlecksCache[hook]) { + return this.$$expandedFlecksCache[hook]; + } const flecks = this.lookupFlecks(hook); let expanded = []; + // Expand configured flecks. for (let i = 0; i < flecks.length; ++i) { const fleck = flecks[i]; - // Just the fleck. expanded.push(fleck); - // Platform-specific variants. for (let j = 0; j < this.platforms.length; ++j) { const platform = this.platforms[j]; const variant = join(fleck, platform); @@ -248,7 +217,7 @@ export default class Flecks { } } } - // Expand elided flecks. + // Handle elision. const index = expanded.findIndex((fleck) => '...' === fleck); if (-1 !== index) { if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) { @@ -256,23 +225,28 @@ export default class Flecks { `Illegal ordering specification: hook '${hook}' has multiple ellipses.`, ); } + // Split at the elision point and remove the ellipses. const before = expanded.slice(0, index); const after = expanded.slice(index + 1); + expanded.splice(index, 1); + // Expand elided flecks. + const elided = []; const implementing = this.flecksImplementing(hook); - const all = []; for (let i = 0; i < implementing.length; ++i) { const fleck = implementing[i]; - all.push(fleck); + if (!expanded.includes(fleck)) { + elided.push(fleck); + } for (let j = 0; j < this.platforms.length; ++j) { const platform = this.platforms[j]; const variant = join(fleck, platform); - if (this.fleck(variant) && this.fleckImplementation(variant, hook)) { - all.push(variant); + if (this.fleck(variant) && !expanded.includes(variant)) { + elided.push(variant); } } } - // Map the elided fleck implementations to vertices in a dependency graph. - const graph = this.flecksHookGraph(without(all, ...before.concat(after)), hook); + // Map the fleck implementations to vertices in a dependency graph. + const graph = this.flecksHookGraph([...before, ...elided, ...after], hook); // Check for cycles. const cycles = graph.detectCycles(); if (cycles.length > 0) { @@ -284,8 +258,12 @@ export default class Flecks { }`, ); } - // Sort the graph and place it. - expanded = [...before, ...graph.sort(), ...after]; + // Sort the graph. + expanded = [ + ...before, + ...graph.sort().filter((fleck) => !expanded.includes(fleck)), + ...after, + ]; } // Build another graph, but add arcs connecting the final ordering. If cycles exist, the // ordering violated the expectation of one or more implementations. @@ -299,7 +277,7 @@ export default class Flecks { if (cycles.length > 0) { cycles.forEach(([l, r]) => { const lImplementation = this.fleckImplementation(l, hook); - const {before: lBefore = [], after: lAfter = []} = lImplementation[HookOrder] || {}; + const {before: lBefore = [], after: lAfter = []} = lImplementation?.[HookPriority] || {}; const explanation = [hook]; if (lBefore.includes(r)) { explanation.push(l, 'before', r); @@ -308,19 +286,24 @@ export default class Flecks { explanation.push(l, 'after', r); } const rImplementation = this.fleckImplementation(r, hook); - const {before: rBefore = [], after: rAfter = []} = rImplementation[HookOrder] || {}; + const {before: rBefore = [], after: rAfter = []} = rImplementation?.[HookPriority] || {}; if (rBefore.includes(l)) { explanation.push(r, 'before', l); } if (rAfter.includes(l)) { explanation.push(r, 'after', l); } - debug("Suspicious ordering specification for '%s': '%s' expected to run %s '%s'!", ...explanation); + debug( + "Suspicious ordering specification for '%s': '%s' expected to run %s '%s'!", + ...explanation, + ); }); } // Filter unimplemented. - return expanded + this.$$expandedFlecksCache[hook] = expanded // eslint-disable-line no-return-assign .filter((fleck) => this.fleckImplementation(fleck, hook)); + debugSilly("cached hook expansion for '%s': %O", hook, expanded); + return this.$$expandedFlecksCache[hook]; } /** @@ -374,20 +357,21 @@ export default class Flecks { .forEach((fleck) => { graph.ensureTail(fleck); const implementation = this.fleckImplementation(fleck, hook); - if (implementation[HookOrder]) { - if (implementation[HookOrder].before) { - implementation[HookOrder].before.forEach((before) => { + if (implementation?.[HookPriority]) { + if (implementation[HookPriority].before) { + implementation[HookPriority].before.forEach((before) => { graph.addArc(fleck, before); }); } - if (implementation[HookOrder].after) { - implementation[HookOrder].after.forEach((after) => { + if (implementation[HookPriority].after) { + implementation[HookPriority].after.forEach((after) => { graph.ensureTail(after); graph.addArc(after, fleck); }); } } }); + this.invoke('@flecks/core.priority', graph, hook); return graph; } @@ -735,6 +719,24 @@ export default class Flecks { return instance.dispatch.bind(instance); } + /** + * Scedule the priority of a hook implementation. + * + * @param {function} implementation + * @param {object} schedule + */ + static priority(implementation, schedule = {}) { + const normalized = {}; + if (schedule.after) { + normalized.after = Array.isArray(schedule.after) ? schedule.after : [schedule.after]; + } + if (schedule.before) { + normalized.before = Array.isArray(schedule.before) ? schedule.before : [schedule.before]; + } + implementation[HookPriority] = normalized; + return implementation; + } + /** * Provide classes for e.g. {@link Flecks#gather} * diff --git a/packages/core/test/one/index.js b/packages/core/test/one/index.js index 9861b70..eb82fb3 100644 --- a/packages/core/test/one/index.js +++ b/packages/core/test/one/index.js @@ -26,8 +26,11 @@ export const hooks = { '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => { resolve({foo: 69}); }), '@flecks/core/test/invoke-merge-unique': () => ({foo: 69}), '@flecks/core/test/invoke-merge-unique-async': () => new Promise((resolve) => { resolve({foo: 69}); }), - '@flecks/core/test.middleware': Flecks.after(['@flecks/core/two'], () => (foo, next) => { - foo.bar += 1; - next(); - }), + '@flecks/core/test.middleware': Flecks.priority( + () => (foo, next) => { + foo.bar += 1; + next(); + }, + {after: '@flecks/core/two'}, + ), }; diff --git a/packages/core/test/three/index.js b/packages/core/test/three/index.js index 7218d8c..060a8e1 100644 --- a/packages/core/test/three/index.js +++ b/packages/core/test/three/index.js @@ -1,8 +1,8 @@ import {Flecks} from '@flecks/core'; export const hooks = { - '@flecks/core/test.middleware': Flecks.before(['@flecks/core/two'], Flecks.after( - ['@flecks/core/one'], + '@flecks/core/test.middleware': Flecks.priority( () => () => {}, - )), + {after: '@flecks/core/one', before: '@flecks/core/two'}, + ), };