midstream

A fully reactive middleware library good for forms or data transforms

Usage no npm install needed!

<script type="module">
  import midstream from 'https://cdn.skypack.dev/midstream';
</script>

README

midstream

A fully reactive middleware library good for forms or data transforms

Installing

npm install midstream@latest

Usage

Import midstream into your application:

import midstream from 'midstream'

V2 API

// Import midstream
import midstream from 'midstream'

// Create some middleware that returns the updated value or throws an error

// Validation Example, errors populate both the err and errors return value
const isOdd = (v) => {
  if (v % 2 > 0) {
    return v
  }

  throw new Error('value is not odd')
}

// Data modification middleware example
const to2Decimals = (v) => parseFloat(v).toFixed(2)

// Split on space example
const split = (v) => v.split(/\s+/)

// Asynchronous fetch example
const multiplyByRandom = async (v) => {
  let res = await fetch('https://www.random.org/integers/?num=1&min=1&max=10&format=plain&col=1&base=10')
  let n = await res.text()

  return parseFloat(n) * parseFloat(v)
}

// Initialize a midstream object.
const ms = midstream({
  // Initialize with a map of names to middlewares.
  // The first value in the middleware list is used as default if it is not a function.
  // Values that are set on the src object or by setters flow through the list of
  // middleware functions.
  // The value returned by a middleware is fed into the next middleware as they
  // are automatically composed:
  //   Given middlewares [f(n), g(n)]
  //   Setting a value x results in g(f(x)) being sent to the dst object
  odd: [1, isOdd],
  num: [1, to2Decimals],
  str: ['foo boo', split]
  rnd: [2, multiplyByRandom],
}, {
  // dst is the destination for the result of setting a value.
  // It can be an object or a function:
  // - object - set the object's properties based on the names passed into the
  //   middleware configuration.  Example:
  //     setting `src.x = 1` causes dst.x to be set to the result of 1 being
  //     run through the middleware
  // - function - the function is called with the results of computation
  dst: (name, value) => console.log(`${name}: ${value}`)
})

// The midstream object is composed of several parts:
// - src - the object with all the pre-middleware processed values
// - err - the object containing the first error thrown by each middleware array
// - dst - a reference to the destination object or function passed in
let {
  src,
  err,
  dst,
  odd, setOdd,
  num, setNum,
  str: setStr,
  rnd, setRnd,
  run,
  wait,
} = ms

// Default values are not processed immediately.
// use `await run()` to force a complete refresh of dst data from src data
// use `await wait()` to wait until any ongoing async updates are completed

// There a few ways to set a value:
src.odd = 3
// Output: odd: 3

setOdd(2)
// Output: odd: 3

console.log(dst.odd)
// Output: 3

// odd is not updated because destructing assignment doesn't copy getters.

console.log(odd)
// Output: 1

console.log(ms.odd)
// Output: 3

console.log(src.odd)
// Output: 3

// Errors in middleware are stored in err
src.odd = 2

console.log(err.odd)
// Output: value is not odd

console.log(src.odd)
// Output: 3

console.log(dst.odd)
// Output: 3

// Async callbacks can be awaited if using a setter
(async () => {
  // a setter can be awaited
  await setRnd(20)
  // Output: rnd: 40

  // this form cannot be awaited because js syntax does not support it
  src.rnd = 20

  // wait has to be called
  await wait()
  // Output: rnd: 60

  // call run to run all middleware even on default values
  await run()
  // Output:
  //   odd: 3
  //   num: 1.00
  //   str: ["foo", "boo"]
  //   rnd: 0
})()

while(true)

An Example React Component

// import midstream
import midstream from 'midstream'

// Some middleware
const isOdd((v) => {
  if (v % 2 > 0) {
    return v
  }

  throw new Error('value is not odd')
}

// Simple hook for Midstream
const useMidstream = (config) => {
  let dst = {}
  let err = {}

  // standard force rerender hack
  let [tick, setTick] = useState(0)

  let [ms, _] = useState(() => {
    return midstream(config, {
      dst: (name, value) => {
        dst[name] = value
        setTick(tick + 1)
      },
      // err behaves just like dst
      err: (name, value) => {
        err[name] = value
        setTick(tick + 1)
      }
    })
  })

  return [ ms, dst, err ]
}

// Your actual component
(props) => {
  const [ms, dst, err] = useMidstream({ odd: [isOdd] })

  // react call this on every update so odd will be up to date
  const { odd, setOdd } = ms

  return (
    <div>
      <p>Last Odd Number: { dst.odd } </p>
      <input value={odd} onChange={(v) => setOdd(e.target.value) }/>
      { err.odd ? <small>Error: { err.odd }</small> : null }
    </div>)
}

Legacy API:

Create a source, destination, and some error throwing middleware. Values set to source will populate destination if all

const defaults = { a: 1, b: 2 }

// can take synchronous or asynchronous functions
const middleware = {
  // multiple middleware in array
  a: [(x) => {
    if (x === 1) {
      return x
    }

    throw new Error('a not 1')
  }],
  // singular middleware
  c: async (x) => {
    if (x === 3) {
      return x
    }

    await new Promise ((res, rej) => {
      requestAnimationFrame(() => {
        rej(new Error('c not 3'))
      })
    })
  },
}
const dst = { a: 1, b: 2, c: 3 }

let { src, err, dst } = midstream(
  middleware,
  { defaults },
)

// set values and wait for asynchronous result, even synchronous middleware completes on the next tick
src.a = 4
// await src.run('a', 4) to for the awaitable async setter
src.b = 0
src.c = 5

// optionally await src.run('a', 'optional value overwrite'), src.runAll(), or src.runSettle().
// src.test is like run but will not set any destination values even without error
let ret = await src.runSettle()

console.log(ret)
// {
//   a: {reason: new Error('a not 1'), status: 'rejected'},
//   b: {status: 'fulfilled', value: 0},
//   c: {reason: new Error('c not 3'), status: 'rejected'},
// }

console.log(err)
// {
//   a: new Error('a not 1'),
//   c: new Error('c not 3'),
// }

console.log(dst)
// {
//   a: 1,
//   b: 0,
//   c: 3,
// }

console.log(src)
// {
//   a: 4,
//   b: 0,
//   c: 5,
// }

Note: test, run, runAll, and runSettle are all functions.

Running tests

npm install
npm run test
npm run test --watch
npm run test:coverage

Dev mode

When developing you can run:

npm run watch

This will regenerate the build files each time a source file is changed and serve on http://127.0.0.1:5000.

Publishing

npm publish