Context
This project and documentation are work in progress
Example code: src/lib/context
As discussed before, context is a parameter passed to all operations at runtime, providing implementations for the operation's side effects.
It's in the context module that we make all this fit together.
Defining the Context type
Very simply, a context is a dictionary of objects. Each of these objects concerns a particular capability of the app, and typically each one is provided by its own adapter.
For instance, the community library app context looks as follows:
So the backend
is provided by one adapter, the events
by another.
And BackendCtx
(used for persistence) looks like this:
When the lib is assembled into an app, it will require an adapter that provides an implementation of BackendCtx
, for instance a MongoDBAdapter
.
The operation and adapter types
You don't have to understand these pieces of code to use them... but it will help you develop a mental model of how an NTA app fits together.
Operations should be familiar by now. They are the functions that do useful things with domain entities:
That is, given an Input and an Output type, an Operation takes (Context, Input) and returns a Promise of Output. (We know it will be a promise because all operation functions are async).
The adapter type is new:
This looks a tad complicated (luckily we only need write it once). Let's break it down:
[Line 1] A
ContextAdapter
works on typesC
andP
.C
is the context being populated (by default the globalContext
, but can be overriden)P
is an object of any params the adapter needs at runtime (optional)
[Line 2] A
ContextAdapter
will be given an operation and those parameters.The operation has an input and output, hence
I
andO
.The adatper's runtime params are optional, hence the
?: P
[Line 3] It will return both a
ctx
(a partial context, for instance{ backend }
) and anop
function that equals or wraps the operation passed inThe result of an adapter is wrapped in a Promise, because adapter functions are async.
What this means is that we can generate a part of the context on demand, when an operation is requested by a user. The adapter function uses any params to help it along, and populates a field in the Context. It can also wrap the operation function with its own code.
Why would an adapter want to wrap an operation? Consider the case of database transactions. You want a way to start a transaction and either commit or rollback, depending on whether the operation throws an error.
Other use cases include error reporting, logging, and any kind of resource allocation you need to do around the operation.
Why would an adapter need runtime parameters? A use case might be something like transaction logging, where the request is assigned an ID and you want your adapter to pass this on to any internal APIs it calls.
Why do adapters return promises? So they can be written as async functions, making it easy to fit in any non-synchronous function calls.
The mergeAdapter function
So, we have various adapters that can populate different fields on the Context
. How can we combine them?
We need a mergeAdapter
function. This should return a single adapter that, when called, invokes and combines the results of all the adapters originally passed to it.
Here's an example of mergeAdapters
written in a recursive style:
If you find recursion unfamiliar, take a look at this guide by Brandon Morelli.
What's important is that you can call it with a set of individual adapters to create a larger adapter:
Applying the adapter with wrapAdapter
Now we need a way to combine the merged adapter with an operation. We'll use this at the last stage of defining the library. Enter wrapAdapter
:
Again, let's step through this piece by piece:
WrapAdapter is a function that operates on types
I
,O
andCA
.I
is the input to the operationO
is the output of the operation (wrapped in a promise)CA
is any ContextAdapter
It is given an adapter and an operation
It returns a function that takes the input of the operation, and any runtime parameters required by the adapter
Internally, when called, the function calls the adapter and creates a context and wrapped operation
The wrapped operation is called with the context and input, and the result is returned
This is how our dependency injection works. We use higher order functions to replace the boilerplate of generating and passing context ourselves. To the user of an operation wrapped with wrapAdapter
, they seem to just be passing an input and receiving an output. They have no idea about the context.
Now we can put all this together to create the library root. Read on!
Last updated