Home Reference Source Repository

lib/cookiesync.js

import cookie from 'react-cookie'
const should = require('chai').should()

/**
 * Creates a synchronizer which uses cookie polling to transmit objects to other tabs.
 * @param  {string}   key                           The key to synchronize on.
 * @param  {function} action                        The action to run when trigger is executed. Should return the payload to be transmitted to the handlers on other tabs.
 * @param  {function} handler                       The handler which is executed on other tabs when a synchronization is triggered. Argument is the return value of the action.
 * @param  {boolean}  [options.tracing=false]       Option to turn on tracing for debugging.
 * @param  {Object}   [options.logger=console]      The logger to debug to.
 * @param  {string}   [options.logLevel=info]       The log level to trace at.
 * @param  {number}   [options.idLength=8]          The number of characters to use for unique instance ID.
 * @param  {number}   [options.pollFrequency=3000]  The frequency (in milliseconds) to poll at.
 * @param  {string}   [options.path='/']            The path to store the cookie at.
 * @param  {boolean}  [options.secure=false]        Flag to set the cookies secure flag.
 * @param  {boolean}  [options.httpOnly=false]      Flag to set the cookies httpOnly flag.
 * @return {Object}                                 cookiesync instance with start, stop, trigger, isRunning, isFallback, and instanceID properties.
 */
export default function cookiesync(key, action, handler, { tracing = false, logger = console, logLevel = 'info', idLength = 8, pollFrequency = 3000, path = '/', secure = false, httpOnly = false } = {}) {
  should.exist(key)
  should.exist(action)
  should.exist(handler)
  const log = (...args) => tracing ? logger[logLevel](...args) : () => {}
  const cookieOpts = { path, secure, httpOnly }
  const cookieKey = `localsync_fallback_${key}`
  const instanceID = (N => (Math.random().toString(36)+'00000000000000000').slice(2, N+2))(idLength)
  const loadCookie = () => {
    try {
      const value = cookie.load(cookieKey, false)
      if(typeof value !== 'undefined') {
        const { instanceID, payload } = value
        should.exist(instanceID, `cookiesync cookies must have an instanceID associated => ${JSON.stringify(value)}`)
        instanceID.should.be.a('string').and.have.lengthOf(idLength)
        should.exist(payload, `cookiesync cookies must have a payload associated => ${JSON.stringify(value)}`)
      }
      log('cookiesync#loadCookie', value)
      return value
    } catch(err) {
      logger.error(err, `cookiesync#loadCookie => error occurred in cookiesync, wiping cookie with key ${cookieKey}`)
      cookie.remove(cookieKey)
    }
  }
  const saveCookie = (...args) => {
    args.should.be.lengthOf(1)
    const [payload] = args
    const value = { instanceID, payload }
    log('cookisync#saveCookie', instanceID, payload)
    cookie.save(cookieKey, value, cookieOpts)
  }

  let isRunning = false
  const trigger = (...args) => {
    log('cookiesync#trigger', instanceID, ...args)
    const payload = action(...args)
    log('cookiesync#trigger => payload', payload)
    saveCookie(payload)
  }

  let intervalID = null
  const start = () => {
    log('cookiesync#start', instanceID)
    let last = loadCookie()
    if(!last) {
      log('cookiesync#start: nolast', instanceID)
      last = { instanceID }
      saveCookie(last)
    }
    intervalID = setInterval(function() {
      log('cookiesync#poll', instanceID)
      let current = loadCookie()
      if(!current) {
        log('cookiesync#poll: nocurrent', instanceID)
        current = last
        saveCookie(current)
      }
      /** DONT NOTIFY IF SAME TAB */
      if(current.instanceID === instanceID) {
        log('cookiesync#poll: sameinstance', instanceID)
        return
      }

      if(JSON.stringify(last.payload) != JSON.stringify(current.payload)) {
        log('cookiesync#poll: INVOKE|instanceID =', instanceID, '|current.instanceID =', current.instanceID, '|last.instanceID =', last.instanceID, '|last.payload =', JSON.stringify(last.payload), '|current.payload =', JSON.stringify(current.payload))
        handler(current.payload)
        last = current
      } else {
        log('cookiesync#poll: noinvoke|instanceID =', instanceID, '|current.instanceID =', current.instanceID, '|last.instanceID =', last.instanceID, '|last.payload =', JSON.stringify(last.payload), '|current.payload =', JSON.stringify(current.payload))
      }
    }, pollFrequency)
    isRunning = true
  }

  const stop = () => {
    log('cookiesync#stop', instanceID)
    clearInterval(intervalID)
    isRunning = false
  }

  return  { start
          , stop
          , trigger
          , get isRunning () { return isRunning }
          , isFallback: true
          , instanceID
          }
}