plugins/dotted.js

import { getValue, setValue, deleteValue } from '@hacknlove/deepobject'
import { isDifferent } from 'isdifferent'
import { resources } from '../lib/conf'

export var state = {}

/**
 * For each resource whose url is a parent of url, update his value and call his callbacks
 *
 * dotted://foo.bar is a parent of dotted://foo.bar.buz
 * @private
 * @param {string} url
 * @returns {undefined}
 */
export function propagateUp (url) {
  const parentUrl = url.replace(/\.?[^.]*$/, '')
  if (!parentUrl) {
    return
  }
  const resource = resources[parentUrl]

  if (resource) {
    resource.value = getValue(state, resource.url)
    Object.values(resource.callbacks).forEach(cb => cb(resource.value))
  }
  propagateUp(parentUrl)
}

/**
 * For each resource whose url is a child of url, if his value has changed, update it and call his callbacks
 *
 * dotted://foo.bar.buz is a parent of dotted://foo.bar
 * @private
 * @param {string} url
 * @returns {undefined}
 */
export function propagateDown (url) {
  const parent = resources[url]
  const prefix = `${url}.`
  Object.keys(resources).forEach(childUrl => {
    if (!childUrl.startsWith(prefix)) {
      return
    }
    const child = resources[childUrl]
    const newChildValue = getValue(parent.value, childUrl.substr(url.length + 1))

    if (isDifferent(newChildValue, child.value)) {
      child.value = newChildValue
      Object.values(child.callbacks).forEach(cb => cb(newChildValue))
    }
  })
}

export default {
  name: 'dotted',
  regex: /^dotted:\/\/./,

  /**
   * Nothing to refresh. shows a warning in the console
   * @returns {undefined}
   */
  refresh () {
    console.warn('the true source for this plugin is client side, so refresh does nothing')
  },

  /**
   * If the state has not value for this resource.url, creates a new updated state
   * else, set resource.value according to the state
   * @param {object} resource
   */
  getResource (resource) {
    const actualValue = getValue(state, resource.url)
    if (actualValue === undefined) {
      state = setValue(state, resource.url, resource.value)
      propagateUp(resource.url)
      propagateDown(resource.url)
      return
    }
    resource.value = actualValue
  },

  /**
   * Returns the value at the state
   * @param {string} url
   * @returns {object} the value
   */
  get (url) {
    return getValue(state, url)
  },

  /**
   * Updates the resource.value, and propagates up and down
   * @param {object} resource
   * @returns {undefined}
   */
  set (resource) {
    state = setValue(state, resource.url, resource.value)
    propagateUp(resource.url)
    propagateDown(resource.url)
  },

  /**
   * If there is no children resource, delete the value, and propagate up
   * @param {object} resource
   * @returns {undefined}
   */
  clean (resource) {
    const prefix = `${resource.url}.`
    if (Object.keys(resources).some(url => url.startsWith(prefix))) {
      return
    }
    propagateUp(resource.url)
  },

  start () {
    state = {}
  },

  saveResource (url, savedResource) {
    savedResource.preventSave = true
  },

  save () {
    return Object.keys(state).length ? state : undefined
  },

  load (data) {
    state = data
  },

  commands: {
    remove (url) {
      state = deleteValue(state, url)
      propagateUp(url)
      propagateDown(url)
    }
  }
}