text: var Value = require('./value')
  var LazyWatcher = require('./lib/lazy-watcher')
  var isSame = require('./lib/is-same')
  var extend = require('xtend')
  
  module.exports = Struct
  
  var blackList = {
    'length': 'Clashes with `Function.prototype.length`.\n',
    'name': 'Clashes with `Function.prototype.name`\n',
    'set': '`set` is a reserved key of struct\n'
  }
  
  const structs = new WeakSet()
  
  
  function Struct (properties, opts) {
    var object = Object.create({})
    var releases = []
    var binder = LazyWatcher(update, listen, unlisten)
    binder.value = object
  
    if (opts && opts.nextTick) binder.nextTick = true
    if (opts && opts.idle) binder.idle = true
  
    var comparer = opts && opts.comparer || null
  
    var observable = function MutantStruct (listener) {
      if (!listener) {
        return binder.getValue()
      }
      return binder.addListener(listener)
    }
  
    var keys = Object.keys(properties)
    var suspendBroadcast = false
  
    keys.forEach(function (key) {
      if (blackList.hasOwnProperty(key)) {
        throw new Error("Cannot create a struct with a key named '" + key + "'.\n" + blackList[key])
      }
  
      var obs = typeof properties[key] === 'function'
        ? properties[key]
        : Value(properties[key])
  
      object[key] = obs()
      observable[key] = obs
    })
  
    observable.set = function (values, opts) {
      var lastValue = suspendBroadcast
  
      suspendBroadcast = true
      values = values || {}
  
      if (opts && opts.merge) {
        values = extend(object, values)
      }
  
      // update inner observables
      keys.forEach(function (key) {
        if (observable[key]() !== values[key]) {
          observable[key].set(values[key])
        }
      })
  
      // store additional keys (but don't create observables)
      Object.keys(values).forEach(function (key) {
        if (!(key in properties)) {
          object[key] = values[key]
        }
      })
  
      suspendBroadcast = lastValue
      if (!suspendBroadcast) {
        binder.broadcast()
      }
    }
  
    structs.add(observable)
    return observable
  
    // scoped
  
    function listen () {
      keys.map(function (key) {
        var obs = observable[key]
        releases.push(obs(function (val) {
          if (!isSame(val, object[key], comparer)) {
            object[key] = val
            if (!suspendBroadcast) {
              binder.broadcast(object)
            }
          }
        }))
      })
    }
  
    function unlisten () {
      while (releases.length) {
        releases.pop()()
      }
    }
  
    function update () {
      var changed = false
      keys.forEach(function (key) {
        var newValue = observable[key]()
        if (!isSame(newValue, object[key], comparer)) {
          object[key] = observable[key]()
          changed = true
        }
      })
      return changed
    }
  }
  
  Struct.isStruct = function (obj) {
    return structs.has(obj)
  }