@unional/async-context

Asynchronous context for functional style programming

Usage no npm install needed!

<script type="module">
  import unionalAsyncContext from 'https://cdn.skypack.dev/@unional/async-context';
</script>

README

@unional/async-context

NPM version NPM downloads Bundle size

Codecov Codacy Grade Badge Codacy Coverage Badge

Secure, type safe, asynchronous context for functional programming.

In functional programming, it is common to pass in a context object containing dependencies used by the function.

AsyncContext allow these dependencies to be loaded asynchronously.

This is useful in many cases. For example,

Installation

npm install @unional/async-context
# or
yarn add @unional/async-context

Usage

import { AsyncContext } from '@unional/async-context'

const context = new AsyncContext({ key: 'secret key' })
const context = new AsyncContext(Promise.resolve({ key: 'secret key' }))
const context = new AsyncContext(() => ({ key: 'secret key' }))
const context = new AsyncContext(async () => ({ key: 'secret key' }))

await context.get() // => { key: 'secret key' }

The context value must be an object (Record). This allows the context to be extended.

If you provide a handler, it will not be executed until the first context.get() is called. It allows you to wait for user input and change the value/dependency loaded.

If you want to start loading the dependencies immediately, starts the loading and pass in a Promise.

Initialize

You can create an AsyncContext and initialize it later. This allows you to create a context in one part of your system, and initialize it in another part.

import { AsyncContext } from '@unional/async-context'

export const context = new AsyncContext<{ key: string }>()

// in another file
import { context } from './context'

context.initialize({ key: 'secret key' })
context.initialize(Promise.resolve({ key: 'secret key' }))
context.initialize(() => ({ key: 'secret key' }))
context.initialize(() => Promise.resolve({ key: 'secret key' }))

initialize() can only be called when you create the AsyncContext with empty constructor. And it can only be called once. This prevents the context to be replaced.

Note that the data it contains are not frozen. If you want to protect them from tampering, you can use Object.freeze() or immutable library such as immutable.

Extend

You can extends a new context with new or override properties.

import { AsyncContext } from '@unional/async-context'

const ctx = new AsyncContext({ a: 1, b: 'b' })

const newCtx = ctx.extend({ b: 2, c: 3 })
const newCtx = ctx.extend(Promise.resolve({ b: 2, c: 3 }))
const newCtx = ctx.extend(() => ({ b: 2, c: 3 }))
const newCtx = ctx.extend(async () => ({ b: 2, c: 3 }))

await newCtx.get() // => { a: 1, b: 2, c: 3 }

Use Cases

Just-in-time Dependency Loading

Since the handlers are delay executed, you can declare the dependencies you need, and load them only when the function is invoked.

import { AsyncContext } from '@unional/async-context'

function createSettingRoute(context: AsyncContext<IO>) {
  return async (request, response) => {
    const { io } = await context.get()
    response(io.loadSetting())
  }
}

addRoute('/setting', createSettingRoute(new AsyncContext(() => ({ io: createIO() }))))

Chained Dependency Loading

You can use extend() to chain asynchronous dependency loading.

import { AsyncContext } from '@unional/async-context'

const ctx = new AsyncContext(() => ({ io: createIO() }))

ctx.extend(loadConfig).extend(loadPlugins)

async function loadConfig(ctx: AsyncContext<{ io: IO }>) {
  const { io } = await ctx.get()
  return { config: await io.getConfig() }
}

async function loadPlugins(ctx: AsyncContext<{ config: Config, io: IO }>) {
  const { config, io } = await ctx.get()
  return { plugins: await Promise.all(config.plugins.map(p => loadPlugin(io, p)) }
}

Configuration Injection

You can wait for user input.

import { AsyncContext } from '@unional/async-context'

let configure: (value: any | PromiseLike<any>) => void
const configuring = new Promise(a => configure = a)

const ctx = new AsyncContext(configuring)

async function doWork(ctx: AsyncContext) {
  const { config } = await ctx.get() // will wait for `configure()`
  ...
}

doWork(ctx)

// call by user after application starts
configure({ ... })