How adapters work
This project and documentation are work in progress
Example code: src/adapters
Adapters are functions that do two things:
they create part of the Context used by operations
they can wrap operations in their own functions (optional)
In the case of the PostgreSQL adapter, we do both, creating a ctx
with database functions, and an op
that wraps a given operation in a transaction:
Let's walk through this code sample:
The adapter is wrapped in
$adapter
- in this codebase a dollar signifies a partially applied / higher order function. So$adapter()
returns the functionadapter
.In the closure of the partially applied function, we create a database connection pool. This will be shared by all invocations of
adapter
.The
adapter
is called with anop
(operation)Note that if the adapter needed runtime parameters, they would be the second argument
The adapter then awaits on a database connection
We return a record with two fields:
an
op
that wraps the provided operation in a database transaction;a
ctx
that exposes functions which ultimately run SQL statements
You'll notice that the context functions are themselves the product of another function called withClient
. We'll talk about this further down the page.
Sharing resources between adapter calls
The reason we use a partially applied function called $adapter
is because that provides the perfect environment for us to share things between adapter calls. Remember that the adapter
is called every time a user request comes in. The more work you can do in the outer closure, the better.
A note on performance
You might wonder about the performance of re-creating the context on every request. Doesn't recreating all those objects and functions create loads of overhead? In practice, however, V8 is pretty smart about reusing things like anonymous functions and structs with a stable shape (provided they come from the same site in the source text).
Node optimisations can mean that certain performance 'tricks' have unexpected effects. For instance, you might be forgiven for assuming that creating a single function and re-calling Function.prototype.bind
would be much more efficient than re-constructing the whole anonymous function. Surely, you'd think, doesn't it save a load of allocation? Actually, usually the latter is more performant, both from a runtime and a GC perspective, because it's an optimisation path V8 recognises and can inline away.
For this reason, be sure to accompany any performance tuning you do with actual tests.
How are adapters written?
Ultimately it's up to you how you want to structure your adapters, but NTA does offer a suggestion:
write 'core functions' that have dependencies injected, just like operation functions
write 'wrapper functions' that can inject those dependencies into the core methods
use a folder structure that matches the structure of your adapter context object
For instance, an adapter 'core function' might look like this one, taken from the MemoryDB adapter in the example project:
Just like with an operation, we don't directly depend on our storage concern. Instead findBook has it injected at a higher level of the adapter. To provide it we create a withDB
function:
What is a MemoryDB
? It's just a type defined inside the adapter folder itself
The advantage of this is that it makes findBook
very easy to test:
Then in our adapter code, we can use withDB
to do the dependency injection:
Last updated