Skip to content

New Diffs: 3 layer frontend architecture proposal

In order to support the proposed shift to server side rendering of the diffs we need to reconsider our current architecture so that it works best with the diffs rendered on the server. I propose the new 3 layer architecture for diffs frontend which will consist of 3 layers:

  1. Diff wrapper (Web Component)
  2. Plugins
  3. Components

Layers can only communicate with the sibling layer. For example a component can communicate with a plugin but can not with a diff wrapper.

flowchart TD
    A[Diff wrapper] -->|Context| B(Plugins)
    B -->|Method calls| A
    B -->|Props| C(Components)
    C -->|Events| B

Diff wrapper

The purpose of the Diff wrapper is to provide:

  1. Data
  2. Low-level DOM manipulation abstractions
  3. Notify on diff element events (mounted, visible, etc.)
  4. Host plugins

Plugins

Plugins is an adapter-based layer which is necessary for connecting Diff wrapper with Components. Having a plugin layer provides us with better decoupling from the actual interface of the diff wrapper. It also hides the complexity of the integration between components and diffs.

Components

Components are modules that solve user tasks: handle discussions, show code quality findings, expand lines, etc. They can be anything: Vue components, pure JS functions, 3rd party modules. They communicate with diffs through Plugins system using primarily events and props.

Example code

The following code is just for general guidance, the final implementation might look very different to that.

Diff wrapper

import { DiffDiscussionsPlugin } from '../dicsussions/plugin';

const DIFF_PLUGINS = [DiffDiscussionsPlugin];

class DiffWrapper extends HTMLElement {
  constructor() {
    DIFF_PLUGINS.forEach(plugin => plugin.instantiate(this));
  }

  connectedCallback() {
    this.dispatchEvent(new CustomEvent('mounted'));
  }
}

Plugin

import Vue from 'vue';
import { discussionsStore } from './store';
import { DiffDiscussions } from './component';

export class DiffDiscussionPlugin {
  instantiate(context) {
    context.addEventListener('mounted', () => {
      const diffHasDiscussions = state => state.discussions.filter(discussion => discussion.file_hash === context.file_hash);
      discussionsStore.watch(diffHasDiscussions, (diffDiscussions) => {
        diffDiscussions.forEach(diffDiscussion => {
          const el = context.extendRow(diffDiscussion.row);
          const remove = () => context.reduceRow(diffDiscussion.row);
          new Vue({
            el,
            render(h) {
              return h(DiffDiscussions, { props: { diffDiscussion }, on: { remove } });
            }
          });
        });
      }, { immediate: true });
    });
  }
}
Edited by Stanislav Lashmanov