Source

Top Overview Utils Interfaces Classes Author

Overview

espo: extensions for stateful programming in JavaScript (OOP utils). Source

Utilities for stateful, OO, and reactive programming: mutations, inheritance/mixins, queing, reactive data sources, implicit subscriptions. Work in progress.

See the sibling library fpx for functional programming utils.

Install with npm:

npm i --save espo

All examples imply an import:

const {someFunction} = require('espo')

On this page, all espo words are exported into global scope. You can run examples in the browser console.


Utils

global

Current global context. In browser, it’s window, in Node.js, it’s global, in webworkers, it’s self, and so on.


isMutable(value)

True if value can be mutated (add/remove/modify properties). This includes most objects and functions. False if value is frozen or a primitive (nil, string, number, etc).

isMutable({})                  =   true
isMutable(isMutable)           =   true
isMutable(null)                =   false
isMutable(Object.freeze({}))   =   false

isImplementation(iface, value)

Rough approximation of X implements interface Y from statically typed languages. Takes an object emulating an “interface” and a value that needs to be tested. True if value has at least the same properties as iface, with the same types; ignores any other properties on value.

Duck-typed replacement for instanceof, particularly useful in “class” constructors.

const iface = {prop: 'blah', method () {}}
isImplementation(iface, {})                                 =  false
isImplementation(iface, {prop: 'blah blah', method () {}})  =  true

// Usage in constructors

function A () {
  if (!isImplementation(A.prototype, this)) return new A()
}

assign(A.prototype, {someMethod () {}})

function B () {
  if (!isImplementation(B.prototype, this)) return new B()
  A.call(this)
}

// B doesn't inherit directly from A; it uses A as a mixin.
// Therefore, the B constructor wouldn't work properly if A used an `instanceof` check.
assign(B.prototype, A.prototype)

// This succeeds because isImplementation(A.prototype, B()) = true
const b = B()

bindAll(object)

Finds all properties of object that are functions and binds them to object so they become bound methods and can be freely detached.

// Setup
const object = {self () {return this}}
object.self() === object

// Detached methods don't work if unbound
const self = object.self
self() === global

// After `bindAll`, detached methods work
bindAll(object)
const boundSelf = object.self
boundSelf === object

final(object, key, value)

Like const, but for object properties. Like let in Swift or final in Java. Defines a property that can’t be reassigned or deleted. Returns object.

const object = {}
final(object, 'one', 1)
object.one === 1
object.one = 10  // exception in strict mode

assign(target, ...sources)

Mutates target, assigning enumerable properties (own and inherited) from each source. Returns target.

Mutation is often misused. It’s necessary when dealing with prototypes, but when working with data, you should program in a functional style. Use a library like Emerge for functional transformations.

assign()                        =  {}
assign({})                      =  {}
assign({}, {one: 1}, {two: 2})  =  {one: 1, two: 2}

push(list, value)

Mutates list, appending value. Similar to list.push(value), but takes exactly one argument and returns list.

push([10], 20) = [10, 20]

pull(list, value)

Mutates list, removing one occurrence of value, comparing values with fpx.is . Returns list.

pull([10, 20], 10) = [20]

setIn(object, path, value)

Mutates object, assigning value at path, where path is a list of keys. If the path doesn’t exist, it’s created as a series of nested dicts. Returns value.

You should never use this for data. When dealing with data, you should program in a functional style, using a library like Emerge.

const tree = {}
setIn(tree, ['one', 'two'], 100)
// tree is now {one: {two: 100}}

redef(storage, path, reconstructor)

Like setIn, but accepts a reconstructor function that will receive the current value at path and return the new value to be set. Returns the resulting value.

function report (value) {console.info(value)}
const que = redef(global, ['dev', 'que'], que => que || Que(report))

defonce(storage, path, constructor, ...args)

Similar to redef, but won’t even call the constructor if a value already exists at path. Accepts additional arguments to pass to the constructor.

function report (value) {console.info(value)}
const que = defonce(global, ['dev', 'que'], Que, report)

valueDescriptors(values)

Converts a dict of properties into enumerable property descriptors for Object.create or Object.defineProperty.

Object.create(somePrototype, valueDescriptors({
  someProperty: 100,
  someMethod () {},
}))

hiddenDescriptors(values)

Converts a dict of properties into non-enumerable property descriptors for Object.create or Object.defineProperty.

Object.create(somePrototype, hiddenDescriptors({
  someProperty: 100,
  someMethod () {},
}))

subclassOf(Superclass, Subclass)

Utility for manual inheritance. Makes the given subclass constructor inherit from the given superclass. This includes instance properties, instance methods, static properties, static methods.

Unless you know you want this, use ES2015 classes instead.

function Super () {
  if (!(this instanceof Super)) return new Super(...arguments)
  bindAll(this)
}

// Instance
assign(Super.prototype, {
  instanceProp: '<my instance prop>',
  instanceMethod () {},
})

// Statics
assign(Super, {
  staticProp: '<my static prop>',
  staticMethod () {},
})

function Sub () {
  if (!(this instanceof Sub)) return new Sub(...arguments)
  Super.apply(this, arguments)
}

subclassOf(Super, Sub)

// Sub now has instance and static props from Super

subclassWithProps(Superclass, props)

Creates a new subclass of Superclass with props added to its prototype. The resulting subclass may be called without new.

const Subclass = subclassWithProps(SomeSuperclass, {
  subProp: '<my instance prop>',
  subMethod () {},
})

const sub = Subclass()

subclassBy(getProps)

Creates a function that will accept a superclass and produce a subclassWithProps with the result of calling getProps with the superclass.

Useful when developing an API with customisable class transforms.

const transform = subclassBy(Superclass => {
  const {prototype: {someMethod}} = Superclass
  return {
    someProp: '<my instance prop>',
    someMethod () {
      // super
      someMethod.apply(this, arguments)
    },
  }
})

const Subclass = transform(SomeSuperclass)

const sub = Subclass()

hackClassBy(getProps)

Similar to subclassBy. Creates a function that will accept a superclass, but instead of creating a new subclass, it will assign the result of getProps directly to its prototype.

Useful when developing an API with chainable class transforms, when you don’t care about “losing” the original superclass. Should cost less memory and performance than subclassBy. Requires care: if one of the methods you’re hacking dynamically reads its “super” method from the same prototype, you’ll get infinite recursion. In this case, simply swap this for subclassBy.

const transform = hackClassBy(Superclass => {
  const {prototype: {someMethod}} = Superclass

  return {
    someProp: '<my instance prop>',
    someMethod () {
      // super
      someMethod.apply(this, arguments)
    },
  }
})

const Subclass = transform(SomeSuperclass)

Subclass === SomeSuperclass  // true

const sub = Subclass()

Interfaces

Espo’s “interfaces” are abstract definitions and, at the same time, boolean tests for class instances.

isDeconstructible(value)

Defines an object with a deconstructor method. It should be the opposite of constructor: deinitialise the object into inert state.

See Deconstructor.

interface Deconstructible {
  deconstructor(): void
}

isDeconstructible({})
// false

class Deconstructible {
  constructor () {
    this.state = acquireExternalState()
  }

  deconstructor () {
    this.state.free()
    this.state = null
  }
}

isDeconstructible(new Deconstructible())
// true

isReactiveSource(value)

See Atom and Subber.

interface ReactiveSource {
  read(query): any
  addSubscriber(subscriber): removeSubscriber
  removeSubscriber(subscriber): void
}

isReactiveSource({})
// false

isReactiveSource(new Atom())
// true

Classes

Espo provides several utility classes. They’re subclassable in ES2015. Properties and methods are enumerable and instance-bound.

Que(deque)

Synchronous, unbounded, FIFO queue. Takes a deque function that will process the values put on the queue in a strict linear order. The calls to deque never overlap.

function deque (value) {
  if (value === 'first') {
    que.push('second')
    que.push('third')
  }
  console.info(value)
}

const que = new Que(deque)

que.push('first')

// prints:
// 'first'
// 'second'
// 'third'

que.push(value)

Adds value to the end of the queue. It will be processed by deque after all other values that are already in the queue. If the que is not dammed, this automatically triggers .flush().

Returns a bound que.pull for value (see below).

function deque (value) {
  if (value === 'first') {
    que.push('second')
    que.push('third')
  }
  console.info(value)
}

const que = new Que(deque)

const abortFirst = que.push('first')

// prints:
// 'first'
// 'second'
// 'third'

que.pull(value)

Removes the first occurrence of value from que, using fpx.is for equality checks. Has no effect if all occurrences of value have already been dequed.

function deque (value) {console.info(value)}
const que = new Que(deque)
que.dam()
const abort = que.push('test')
abort()
que.flush()
// nothing happens

que.dam()

Pauses an idle que. A dammed que accumulates values added by .push(), but doesn’t flush automatically. This allows you to delay processing, batching multiple values. Call .flush() to unpause and resume processing.

Has no effect if the que is already flushing at the time of the call.

function deque (value) {console.info(value)}

const que = new Que(deque)

que.dam()

// nothing happens yet
que.push('first')
que.push('second')

// prints 'first' and 'second'
que.flush()

que.flush()

Unpauses and resumes processing. You only need to call it after .dam().

que.isEmpty()

Self-explanatory.


TaskQue()

Special case of Que: a synchronous, unbounded, FIFO task queue. You put functions on it, and they execute in a strict linear order.

const taskQue = new TaskQue()

function first () {
  console.info('first started')
  taskQue.push(second)
  console.info('first ended')
}

function second () {
  console.info('second')
}

taskQue.push(first)

// prints:
// 'first started'
// 'first ended'
// 'second'

taskQue.push(task, ...args)

See example above.

Adds task to the end of the queue. It will be called with args as arguments and taskQue as this after executing all other tasks that are already in the queue. If the que is not dammed, this automatically triggers .flush().

Returns a function that removes the task from the que when called.

const taskQue = new TaskQue()
taskQue.dam()
const abort = taskQue.push(function report () {console.info('reporting')})
abort()
taskQue.flush()
// nothing happens

taskQue.dam()

See que.dam().

taskQue.flush()

See que.flush().

taskQue.isEmpty()

See que.isEmpty().


Atom(state)

Satisfies isReactiveSource.

Very similar to clojure.core/atom.

Conceptually, it can be viewed as:

You can use Atom as a unit of reactivity in JS applications, particularly as a substitute for Redux. In this case, it’s highly recommended to pair it with Emerge for efficient functional transformations of nested data structures.

const atom = new Atom(10)

const removeSubscriber = atom.addSubscriber((atom, prev, next) => {
  console.info(prev, next)
})

atom.swap(value => value + 1)
// reports 10, 11

atom.swap(value => value + 100)
// reports 11, 111

removeSubscriber()

atom.state

Current value of atom.

atom.swap(mod, ...args)

where mod = ƒ(atom.state, ...args)

Calls mod with the current value of the atom and the optional extra args. Resets atom to the resulting value and notifies the subscribers. Returns the newly committed state.

Swap commits the new state immediately, before it returns. However, subscriber notifications are put on an internal TaskQue so that they never overlap. This means that if you’re calling swap inside an ongoing subscriber notification, the next notification will happen “asynchronously” in relation to this code, even though it runs in the same call stack.

const atom = new Atom(10)
// atom.state = 10
const add = (a, b, c) => a + b + c
const newState = atom.swap(add, 1, 2)
// newState = atom.state = add(10, 1, 2) = 13

atom.addSubscriber(fun)

where fun = ƒ(atom, prevState, nextState)

Registers fun as a subscriber that will be called on each state transition. Returns a function that removes fun from the subscribers when called.

atom.removeSubscriber(fun)

Removes fun from the subscribers.


Subber()

Satisfies isDeconstructible.

Utility for establishing multiple subscriptions to multiple reactive data sources, implicitly, without any syntactic noise. It allows you to run a function that simply tries to read the data, which becomes the basis of subscriptions.

const holly = new Atom({left: 10})
const molly = new Atom({right: 20})

const subber = new Subber()

function reader (subber) {
  return subber.read(holly, ['left']) * subber.read(molly, ['right'])
}

function updater (subber) {
  console.info('new value:', subber.run(reader, updater))
}

const value = subber.run(reader, updater)

value === 200
// true

subber.value === value
// true

holly.swap(state => ({left: state.left * 3}))
// $ 'got notified'

subber.value === 600

read(source, query)

where source: isReactiveSource

Queries source with query via source.read(). Implicitly establishes a reactive subscription when called within a subber.run() (see below).

run(reader, updater)

where reader: ƒ(subber), updater: ƒ(subber)

Calls reader, passing self as the argument. During this call, every call to subber.read() implicitly establishes a reactive subscription for the given source/query pair. Multiple .read() calls create multiple subscriptions. Whenever the result of any of these queries changes, the subber will remove all existing subscriptions and call updater, which is free to call subber.run() and restart the cycle.

It’s ok to subber.run() again before the updater has been called. It will forget the previous subscriptions and the previous updater, replacing them with the new ones.


Deconstructor()

Satisfies isDeconstructible.

Aggregator of deconstructibles that you assign directly onto it. It will deconstruct each of them when destroyed.

const dc = new Deconstructor()

dc.lc = new Lifecycler()

// nesting works
dc.otherDc = new Deconstructor()

dc.otherDc.lc = new Lifecycler()

dc.deconstructor()

// both dc's are now empty, and both lc's have been deconstructed

Lifecycler()

Satisfies isDeconstructible.

Utility for reversible initialisation and reinitialisation.

TODO document motivation and usage.

initer, reiniter, deiniter all have the same shape: ƒ(lifecycler): void

init(initer)

reinit(reiniter)

deinit([deiniter])

onDeinit(deiniter)

Lifecycler.init(initer)


FixedLifecycler(config)

Satisfies isDeconstructible.

Version of Lifecycler for when initer and deiniter never change.

TODO document motivation and usage.

config:

interface {
  initer(fixedLifecycler): void
  deiniter(fixedLifecycler): void
}

init()

reinit()

deinit()


Author

mitranim.com