ZooKeeper JavaScript Implementation

ZooKeeper JavaScript Implementation

Def from Apache: ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services.

ZooKeeper Node Class Structure:

class ZooNode {
  key = null;
  value = null;
  watchers = [];
  children = [];
  constructor(key, value, watcherFn) {
    this.key = key;
    this.value = value;
    if (watcherFn) {
      this.watchers.push(watcherFn);
    }
  }
}

ZooKeeper Implementation:


class ZooKeeper {
  root = new ZooNode('root');

  /*get path segments*/
  getPathSegments(path) {
    const ps = path.split('/');
    ps.shift();
    return ps;
  }

  /*call watchers*/
  callWatchers(fns) {
    fns.forEach(fn => fn?.());
  }

  /*find a node and it's watchers*/
  dfs(root, pathSegments, idx, watcherFns = []) {
    if (!pathSegments.length) {
      return { node: root, watcherFns: [...root.watchers] };
    }
    const key = pathSegments[idx];
    watcherFns = watcherFns.concat(root.watchers);
    const nodeIdx = root.children.map(p => p.key).indexOf(key);
    if (nodeIdx > -1) {
      const nextRoot = root.children[nodeIdx];
      if (pathSegments.length === idx + 1) {
        watcherFns = watcherFns.concat(nextRoot.watchers);
        return { node: nextRoot, watcherFns };
      }
      return this.dfs(nextRoot, pathSegments, idx + 1, watcherFns);
    }
  }

  create(path, value) {
    const ps = this.getPathSegments(path, true);
    const newChildKey = ps.pop();
    const { node: parentNode, watcherFns } = this.dfs(this.root, ps, 0) || {};
    if (parentNode) {
      parentNode.children.push(new ZooNode(newChildKey, value));
      this.callWatchers(watcherFns);
    } else {
      console.log(`unable to add, path is not exist: ${path}`);
    }
  }

  setValue(path, value) {
    const ps = this.getPathSegments(path, true);
    const { node, watcherFns } = this.dfs(this.root, ps, 0) || {};
    if (node) {
      node.value = value;
      this.callWatchers(watcherFns);
    } else {
      console.log(`unable to update, path is not exist: ${path}`);
    }
  }

  getValue(path) {
    const ps = this.getPathSegments(path, true);
    const { node } = this.dfs(this.root, ps, 0) || {};
    if (node) {
      return node.value;
    } else {
      console.log(`unable to update, path is not exist: ${path}`);
    }
  }

  watch(path, fn) {
    const ps = this.getPathSegments(path, true);
    const { node } = this.dfs(this.root, ps, 0) || {};
    if (node) {
      node.watchers = [fn];//concat for multiple listeners
    } else {
      console.log(`unable to update, path is not exist: ${path}`);
    }
  }
}

Execution:

let z = new ZooKeeper();
z.create('/a', 'foo');
z.create('/a/c', 'bar');
z.create('/a/d', 'foo_d');
z.setValue('/a/d', 'foo_d_update');
console.log(z.getValue('/a/d')); 
//foo_d_update

z.watch('/a/d', () => { console.log('a watch at /a/d') });
z.watch('/a', () => { console.log('a watch at /a') });
z.setValue('/a/d', 'foo_d_update_again'); 
//1. a watch at /a 
//2. a watch at /a/d

z.setValue('/a', 'foo_a_update_again');
//a watch at /a