Skip to content

Conflict management

James Anthony Bruno edited this page Apr 25, 2018 · 1 revision

Conflict management

⚠️ This article has a mix of developer documentation and general information. Conflict management API is not expected to be used or understood by projects using Nilbog.

Table of contents

Why

One potential issue with Nilbog is that multiple observers operating on the same element could create a feedback loop. Example lifecycle without conflict management:

  • Observer A is created, protecting text on elements that match .class-a.
  • Observer B is created, protecting text on elements that match .class-b.
  • Element with classes .class-a.class-b has its text mutated.
    • Observer A catches this.
      • Reverts text
        • Observer B notices the reversion
          • Reverts reversion
            • Observer A notices
              • ad infinitum

This will continue forever, exhausting the user's CPU and probably crashing the page (although browsers can be slow on killing the page when this occurs).

This is why Nilbog has a safe mode that implements conflict management.

How

Here is an example lifecycle with safe mode on:

  • Nilbog observer is created (for example, nilbog.protectText('.protect-text'))
    • This observer is registered with the ConflictManager
      • The ConflictManager assigns it to the appropriate ObserverList (in this case, the protectText observer list).
  • Mutation event occurs:
    • Before operating on the element, the observer asks the ConflictManager to resolve any conflicts that might exist.
      • The ConflictManager tells the appropriate ObserverList to resolve conflicts.
        • The ObserverList identifies conflicting observers that are watching on the same parent, are connected, and match with mutated element.
        • If any conflicts exist, the ObserverList applies some sort of tiebreaker.
          • Current tiebreaker sorts the observers by uid and returns the first in the list. This is repetable (same observer on subsequent calls) but not replicable (on a different page load, the uid will be different so a different observer might be called).
        • If the observer has no conflicts or won the tiebreaker, returns true. Otherwise returns false.
      • ConflictManager passes on the ObserverList's decision.
    • If the ConflictManager returns true, the observer is cleared to operate. Otherwise, the observer is told to refrain.

Special cases

Different types of observers have specific ways of resolving conflicts.

preventCreate and preventDelete

If there's an observer of both of these types that operates on the same node, a feedback loop may occur. In the case that such a conflict is found, the observers are invariably told to do nothing, allowing the mutation to stay.

Changelog type observers: protectClasses and protectAttributes

Both of these types of observers can choose to operate on specific changes (either created, deleted, or modified) and properties (for example, preventing creation of certain attributes). Unless there are no conflicts at all or the observer is the ultimate winner of all conflicts, conflicts are resolved on a case-by-case basis.

API

ConflictManager class

Routes resolution requests and registration to the appropriate ObserverList.

Methods

new ConflictManager(noop)

Initialize ConflictManager.

Arguments
  • noop (optional, default: false) - noop is true when safe mode is on. Essentially doesn't register any observers and always resolves true.
conflictManager.register(type, observer)

Register observer in the appropriate ObserverList. If noop, does nothing.

Arguments
  • type (required) - Observer type. Either preventCreate, preventDelete, protectText, protectAttributes, or protectClasses.
  • observer (required) - Observer instance.
conflictManager.resolve(action, observer, node, ...extras)

Ask conflict manager to resolve any conflicts with observed mutation. If noop, always gives the go-ahead.

Arguments
  • action (required) - Observer type. Either preventCreate, preventDelete, protectText, protectAttributes, or protectClasses.
  • observer (required) - Inquiring Observer instance.
  • node (required) - Matched node that the observer wants to revert.
  • extras (optional) - Rest of the parameters are collected to an array to be passed on.
Returns
  • true - If noop is true
  • Whatever the ObserverList resolves with

Properties

ObserverList class

Collection of observers

Methods

new ObserverList()

Initialize observer list.

observerList.add(observer)

Add to observer list.

Arguments
  • observer (required) - Observer to add to list.
observerList.conflicts(observer, node)

Get a list of conflicting observers that may also want to operate on the node.

Arguments
  • observer (required) - Inquiring Observer
  • node (required) - DOM element wanting to be operated on.
Returns
  • List of conflicting observers. Criteria:
    • not equal to observer
    • currently observing
    • matches node
    • parents are either equal or have parent-child relationship
observerList.tiebreaker(observer, conflicts)

Determines whether observer gets to operate based on conflicting observers. Implementation determines this by sorting the observers by their unique ID, and picking the first one.

Arguments
  • observer (required) - Inquiring Observer
  • conflicts (required) - Array of conflicting Observers
Returns
  • true - Won the tiebreaker
  • false - Lost the tiebreaker
observerList.resolve(observer, node)

Resolves conflicts if they exist.

Arguments
  • observer (required) - Inquiring Observer
  • node (required) - DOM element wanting to be operated on.
Returns
  • true - Go ahead and operate
  • false - Don't operate, a different Observer might operate

Properties

  • observers - Array of Observers

PreventObserverList class (extends ObserverList)

ObserverList for preventCreate and preventDelete.

Inherited

Methods
  • conflicts(observer, node)
  • tiebreaker(observer, conflicts)
Properties
  • observers

Methods

new PreventObserverList(error)

Creates new observer list.

Arguments
  • error (required) - Warning displayed in the console when a conflict between preventCreate and preventDelete observers occur.
preventObserverList.resolve(observer, node, other)

Resolve conflicts if they exist. Overrides observerList.resolve(observer, node).

Arguments
  • observer (required) - Inquiring Observer
  • node (required) - DOM element wanting to be operated on.
  • other (required) - Counterpart PreventObserverList (preventCreate's for preventDelete and vice-versa).
Returns
  • true - Go ahead and operate
  • false - Don't operate, a different Observer might operate or there was a conflict between preventCreate and preventDelete.

Properties

  • error (default: "Nilbog conflict: preventCreate and preventDelete targeting same node. No action taken.") - Warning to be displayed in console when a conflict between preventCreate and preventDelete observers is found.

ChangelogObserverList class (extends ObserverList)

ObserverList for protectAttributes and protectClasses.

Inherited

Methods
  • constructor
  • conflicts(observer, node)
  • tiebreaker(observer, conflicts)
Properties
  • observers

Methods

changelogObserverList.lowLevelTiebreakers(undo, observer, conflicts)

Based on list on conflicts and what would be undone by the observer, breaks ties on a property-by-property, attribute-by-attribute basis.

Arguments
  • undo (required) - What would be undone by the observer. Each property has an array of strings
    • undo.create
    • undo.delete
    • undo.modify
  • observer (required) - Inquiring Observer
  • conflicts (required) - Conflicting Observers
Returns
  • undo - Input undo modified, removing any attributes that the observer loses in a tiebreaker with conflicts, and retaining uncontested attributes and attributes that were won in tiebreakers.
    • undo.create
    • undo.delete
    • undo.modify
changelogObserverList.resolve(observer, node, undo)

Resolve conflicts if they exist. Will resolve on a property-by-property, attribute-by-attribute basis if conflicts are found. Overrides observerList.resolve(observer, node).

Arguments
  • observer (required) - Inquiring Observer
  • node (required) - DOM element wanting to be operated on.
  • undo (required) - What would be undone by the observer. Each property has an array of strings
    • undo.create
    • undo.delete
    • undo.modify
Returns
  • true - No conflicts or would ultimately win all tiebreakers so go ahead and operate.
  • undo - List of things that are allowed to be undone by this observer. Other observers will be operating on missing changes.
    • undo.create
    • undo.delete
    • undo.modify