Skip to content
Graffle is a work in progress. Learn more.

Anyware

Introduction

Graffle allows you to apply one or more anyware's to the request pipeline. Each anyware runs on every request.

You can think of anyware like middleware (run code before and after some operation with control to manipulate input and output) except that it represents a sequence of operations that form a pipeline rather than just one operation like traditional middleware.

The request pipeline has five hooks: encode, pack, exchange, unpack, and decode.

Each hook receives an input and returns an output. Each hook's output is the input to the next hook, until the last hook which returns the final result.

Here is a snippet with a few highlights to give you a feel for how it works (note it does not always have to be this verbose as you will see later):

ts
Graffle
.
create
({
schema
: '...' })
.
anyware
(async ({
encode
}) => {
const {
pack
} = await
encode
() // [1]
const {
exchange
} = await
pack
({
input
: { ...
pack
.
input
/* ... */ }, // [2]
}) const {
unpack
} = await
exchange
({
using
: {
fetch
: async () => new
Response
() }, // [3]
}) return
unpack
() // [4]
// const { decode } = await unpack() // const result = await decode() // return result })
  1. We run encode without passing any input to it. It runs using its original input.
  2. We run pack with some custom input. We just want to modify the input slightly so access the original input under the input property on the hook function to spread in.
  3. This is leveraging a feature called "slots". We run exchange with its original input but modified implementation for fetch.
  4. This is leveraging a feature called "short-circuiting". We could have run the rest of the hooks manually but if we don't have to. By returning the hook result we're conceptually turning any remaining hooks into automatic passthroughs.

Data

Recall that Graffle supports multiple interfaces and transports:

InterfaceTransport
typedhttp
rawmemory

Discriminated unions are used to model this in the input/output types flowing through the pipeline. While each hook's input is unique to itself there is a base type that all inputs share which carries the discriminated properties, interface and transport. You can use these properties to narrow the type as needed.

ts
Graffle
.
create
({
schema
: 'https://...' })
.
anyware
(async ({
encode
}) => {
if (
encode
.
input
.
interface
=== 'typed') {
// Do something here. } // etc. return
encode
()
})

If Graffle is created with a URL for schema then it will automatically type transport as http. Conversely if you pass a GraphQL schema instance it will automatically type transport as memory.

Hooks

Encode

When InterfaceThen Performs Tasks
typed1. Transform input into its GraphQL representation.
2. Encode custom scalars if any.
rawPassthrough

Pack

When TransportThen Performs Tasks
httpConstruct a Request.
memoryPassthrough

Slot searchParams

When TransportThen Performs Tasks
httpWhen request will be sent using GET this slot is called to create the value that will be used for the HTTP Search Parameters.
memoryN/A

Slot body

When TransportThen Performs Tasks
httpWhen request will be sent using POST this slot is called to create the value that will be used for the HTTP body.
memoryN/A

Exchange

When TransportThen Performs Tasks
httpSend request using fetch.
memorySend request using execute/graphql from the graphql package.

Slot fetch

When TransportThen Performs Tasks
httpSend request using fetch.
memoryN/A

Unpack

When TransportThen Performs Tasks
httpTransform Response into GraphQL result.
memoryPassthrough

Decode

When InterfaceThen Performs Tasks
typedDecode custom scalars if any.
rawPassthrough

Jump-Starting

If you want to jump straight to a specific hook other than encode you can do so by simplify destructing to the desired hook. For example here we write anyware for exchange:

ts
Graffle
.
create
({
schema
: '...' })
.
anyware
(async ({
exchange
}) => {
// ... do something ... const {
unpack
} = await
exchange
()
// ... do something ... const {
decode
} = await
unpack
()
// ... do something ... const
result
= await
decode
()
// ... do something ... return
result
})

Note that you will hit a runtime error if you try to destructure more than one hook.

Short-Circuiting

If you want to end your work before decode you can do so by returning any hook result. This will cause the reset of the hooks to become passthroughs. For example:

ts
Graffle
.
create
({
schema
: '...' })
.
anyware
(async ({
encode
}) => {
// ... do something ... const {
pack
} = await
encode
()
// ... do something ... const {
exchange
} = await
pack
()
// ... do something ... return await
exchange
()
})

Input

You can override the input to any hook by passing your own input:

ts
Graffle
  .create({ schema: '...' })
  .anyware(async ({ encode }) => {
    return encode({
      input: {/* ... */},
    })
  })

You can access the original input of any hook from its input property. This is often useful for spreading into a hook so that you only have to pass the input you want to modify.

ts
Graffle
.
create
({
schema
: '...' })
.
anyware
(async ({
encode
}) => {
return
encode
({
input
: { ...
encode
.
input
/* ... */ },
}) })

Slots

Hooks can have one or more slots that allow you to override a part of their implementation.

When two anywares are used that both customize the same slot then its the later anyware whose customization is used.

Multiple Anywares

You can use as many anywares as you want. Typically you'll only have one explicit one with others coming from extensions you've added.

They execute in the order they were added, regardless of if it was added via an extension or directly. So for example in the following example the direct anyware would run after the first used extension but before the second.

ts
Graffle
  .create({ schema: '...' })
  .use(Extension1WithAnyware())
  .anyware(async ({ encode }) => {/* ... */})
  .use(Extension2WithAnyware())

When running there are two stacks that are interleaved as described below:

  1. Anyware stack
  2. Hook stack

When an anyware runs a hook it actually passes control to the next anyware in the stack to run it but using input coming from that previous anyware. This process repeats until there are no more anywares in the stack at which point the hook is run with the finally produced input and a return value is produced. That value is sent back to the first anyware to continue execution and repeating this cycle for the next hook. Eventually all hooks are run and the final result is returned.

You can access the original input of any hook from its input property. This is often useful for spreading into a hook so that you only have to pass the input you want to modify.

Released under the MIT License.