hologit
Version:
Hologit automates the projection of layered composite file trees based on flat, declarative plans
400 lines (299 loc) • 10.7 kB
JavaScript
const toposort = require('toposort');
const TreeObject = require('./TreeObject.js');
const Configurable = require('./Configurable.js');
const Branch = require('./Branch.js');
const Source = require('./Source.js');
const Lens = require('./Lens.js');
const branchCache = new WeakMap();
const branchMapCache = new WeakMap();
const sourceCache = new WeakMap();
const sourceMapCache = new WeakMap();
const lensCache = new WeakMap();
const lensMapCache = new WeakMap();
class Workspace extends Configurable {
constructor ({ root=null, sources=null, branches=null }) {
if (!root || !(root instanceof TreeObject)) {
throw new Error('root required, must be instance of TreeObject');
}
super(...arguments);
this.root = root;
// flag programmatic construction so the engine dispatcher knows this
// workspace's config is not readable from the tree (lib/RustEngine.js)
this.hasPhantomSources = Boolean(sources);
// Pre-populated sources and branches for programmatic construction
if (sources) {
const cache = new Map();
for (const [name, source] of Object.entries(sources)) {
if (source instanceof Source) {
cache.set(name, source);
} else {
// Accept plain config objects — construct Source with phantom
cache.set(name, new Source({ workspace: this, name, phantom: source }));
}
}
sourceCache.set(this, cache);
}
if (branches) {
const cache = new Map();
for (const [name, branch] of Object.entries(branches)) {
if (branch instanceof Branch) {
cache.set(name, branch);
} else {
cache.set(name, new Branch({ workspace: this, name, phantom: branch }));
}
}
branchMapCache.set(this, cache);
}
Object.freeze(this);
}
getWorkspace () {
return this;
}
getKind () {
return 'holospace';
}
getConfigPath () {
return '.holo/config.toml';
}
async writeWorkingChanges () {
const { root } = this;
if (!root.dirty) {
return;
}
if (!root.repo.workTree) {
throw new Error('cannot write working changes without work tree');
}
const git = await root.repo.getGit();
const originalTreeHash = root.hash;
await root.write();
try {
await git.readTree({ m: true, u: true }, originalTreeHash, root.hash);
} catch (err) {
throw new Error(`failed to apply changes to working tree:\n${err.stderr || err.message}`);
}
}
getBranch (name) {
let cache = branchCache.get(this);
const cachedBranch = cache && cache.get(name);
if (cachedBranch) {
return cachedBranch;
}
// instantiate branch
const branch = new Branch({
workspace: this,
name
});
// save instance to cache
if (!cache) {
cache = new Map();
branchCache.set(this, cache);
}
cache.set(name, branch);
// return instance
return branch;
}
async getBranches () {
// return cached map if available
const cachedMap = branchMapCache.get(this);
if (cachedMap) {
return cachedMap;
}
// read tree
const tree = await this.root.getSubtree(`.holo/branches`);
const children = tree ? await tree.getChildren() : {};
// build unsorted map via recursive discovery
const childNameRe = /^([^\/]+)\.toml$/;
const map = new Map();
for (const childName in children) {
if (children[childName].isTree) {
// skip .lenses directories (e.g. "docs-site.lenses")
if (childName.endsWith('.lenses')) {
continue;
}
// peek inside: if directory contains .toml files it defines
// a branch's mappings; otherwise it's a namespace — recurse
await this._discoverBranches(
children[childName], childName, childNameRe, map
);
} else {
const nameMatches = childName.match(childNameRe);
if (!nameMatches) {
continue;
}
const [,name] = nameMatches;
map.set(name, this.getBranch(name));
}
}
// cache and return map
branchMapCache.set(this, map);
return map;
}
async _discoverBranches (tree, prefix, childNameRe, map) {
const treeChildren = await tree.getChildren();
// check if this directory contains any .toml files directly
let hasTomlFiles = false;
for (const name in treeChildren) {
if (!treeChildren[name].isTree && childNameRe.test(name)) {
hasTomlFiles = true;
break;
}
}
if (hasTomlFiles) {
// directory contains mappings — it defines a branch
map.set(prefix, this.getBranch(prefix));
} else {
// no .toml files — it's a namespace, recurse into subdirectories
for (const name in treeChildren) {
if (!treeChildren[name].isTree || name.endsWith('.lenses')) {
continue;
}
await this._discoverBranches(
treeChildren[name], prefix + '/' + name, childNameRe, map
);
}
}
}
getSource (name) {
let cache = sourceCache.get(this);
const cachedSource = cache && cache.get(name);
if (cachedSource) {
return cachedSource;
}
// instantiate source
const source = new Source({
workspace: this,
name
});
// save instance to cache
if (!cache) {
cache = new Map();
sourceCache.set(this, cache);
}
cache.set(name, source);
// return instance
return source;
}
async getSources () {
// return cached map if available
const cachedMap = sourceMapCache.get(this);
if (cachedMap) {
return cachedMap;
}
// read tree
const tree = await this.root.getSubtree(`.holo/sources`);
const children = tree ? await tree.getChildren() : {};
// build unsorted map
const childNameRe = /^([^\/]+)\.toml$/;
const map = new Map();
for (const childName in children) {
const nameMatches = childName.match(childNameRe);
// skip any file not ending in .toml
if (!nameMatches) {
continue;
}
const [,name] = nameMatches;
map.set(name, this.getSource(name));
}
// cache and return map
sourceMapCache.set(this, map);
return map;
}
/**
* Return an ordered Map of layers, each being an ordered Map of Mappings
*/
async getLayers () {
}
getLens (name) {
let cache = lensCache.get(this);
const cachedLens = cache && cache.get(name);
if (cachedLens) {
return cachedLens;
}
// instantiate lens
const lens = new Lens({
workspace: this,
name
});
// save instance to cache
if (!cache) {
cache = new Map();
lensCache.set(this, cache);
}
cache.set(name, lens);
// return instance
return lens;
}
/**
* Return an order list of Lens objects
*/
async getLenses () {
// return cached map if available
const cachedMap = lensMapCache.get(this);
if (cachedMap) {
return cachedMap;
}
// read tree
const tree = await this.root.getSubtree(`.holo/lenses`);
const children = tree ? await tree.getChildren() : {};
// build unsorted hash
const childNameRe = /^([^\/]+)\.toml$/;
const lenses = {};
for (const childName in children) {
// skip any child not ending in .toml
const filenameMatches = childName.match(childNameRe);
if (!filenameMatches) {
continue
}
// skip any child that is deleted or isn't a blbo
const treeChild = children[childName];
if (!treeChild || !treeChild.isBlob) {
continue;
}
// read lens
const [, name] = filenameMatches;
lenses[name] = this.getLens(name);
}
// compile edges formed by before/after requirements
// TODO: de-dupe this behavior with Branch.getLenses
const edges = [];
for (const name in lenses) {
const lens = lenses[name];
const { after, before } = await lens.getCachedConfig();
if (after) {
for (const afterLens of after) {
if (afterLens == '*') {
for (const otherLens in lenses) {
if (otherLens != name && after.indexOf(otherLens) == -1) {
after.push(otherLens);
}
}
continue;
}
edges.push([lenses[afterLens], lens]);
}
}
if (before) {
for (const beforeLens of before) {
if (beforeLens == '*') {
for (const otherLens in lenses) {
if (otherLens != name && before.indexOf(otherLens) == -1) {
before.push(otherLens);
}
}
continue;
}
edges.push([lens, lenses[beforeLens]]);
}
}
}
// build map of lenses sorted by before/after requirements
const map = new Map();
for (const lens of toposort.array(Object.values(lenses), edges)) {
map.set(lens.name, lens);
}
// cache and return map
lensMapCache.set(this, map);
return map;
}
}
module.exports = Workspace;