What is meros?
The meros npm package is designed to handle multipart responses in a more convenient and efficient way. It is particularly useful for dealing with GraphQL multipart responses, allowing clients to process parts of the response as they arrive, rather than waiting for the entire response. This can improve performance and user experience in applications that consume streaming APIs or need to handle large, multipart responses.
What are meros's main functionalities?
Parsing GraphQL multipart responses
This code sample demonstrates how to use meros to parse a GraphQL multipart response from a fetch request. It checks if the response is of type 'multipart/mixed' and then uses meros to asynchronously iterate over each part of the response, logging the body of each part if it is JSON.
import { meros } from 'meros';
const response = await fetch('/graphql', {
body: JSON.stringify({ query }),
method: 'POST',
});
if (response.headers.get('content-type').includes('multipart/mixed')) {
const parts = await meros(response);
for await (const part of parts) {
if (part.json) {
console.log(part.body);
}
}
}
Other packages similar to meros
graphql-helix
GraphQL Helix is a collection of utility functions for building your own GraphQL HTTP server. It supports multipart responses similar to meros but is more focused on the server-side handling of GraphQL requests, including features for query execution and subscription support.
subscriptions-transport-ws
This package is a transport layer for handling GraphQL subscriptions over WebSocket. While it doesn't directly deal with multipart responses, it offers a real-time data fetching capability that can be seen as an alternative approach to handling streaming data, compared to the multipart response handling in meros.
⚡ Features
- No dependencies
- Super performant
- Supports any1
content-type
- preamble and epilogue don't yield
- Browser-Compatible
- Plugs into existing libraries like Relay and rxjs
⚙️ Install
yarn add meros
🚀 Usage
import { meros } from 'meros';
const parts = await fetch('/api').then(meros);
for await (const part of parts) {
}
from(parts).pipe(
tap((part) => {
}),
);
Specific Environment
import { meros } from 'meros/browser';
const parts = await fetch('/api').then(meros);
import http from 'http';
import { meros } from 'meros/node';
const response = await new Promise((resolve) => {
const request = http.get(`http://my-domain/mock-ep`, (response) => {
resolve(response);
});
request.end();
});
const parts = await meros(response);
🎒 Notes
This library aims to implement RFC1341 in its entirety, however we aren't
there yet. That being said, you may very well use this library in other
scenarios like streaming in file form uploads.
Please note; be sure to define a boundary that can be guaranteed to never
collide with things from the body:
Because encapsulation boundaries must not appear in the body parts being
encapsulated, a user agent must exercise care to choose a unique boundary.
~ RFC1341 7.2.1
meros
comes from Ancient Greek μέρος méros, meaning "part".
Caveats
- No support the
/alternative
, /digest
or /parallel
subtype at this
time. - No support for
nested multiparts
🔎 API
Meros offers two flavours, both for the browser and for node; but their api's
are fundamentally the same.
Note: The type Response
is used loosely here and simply alludes to
Node's IncomingMessage
or the browser's Response
type.
meros(response: Response, options?: Options)
Returns: Promise<Response | AsyncGenerator<Part | Part[]>
Meros returns a promise that will resolve to an AsyncGenerator
if the response
is of multipart/mixed
mime, or simply returns the Response
if something
else; helpful for middlewares. The idea here being that you run meros as a chain
off fetch.
fetch('/api').then(meros);
If the content-type
is NOT a multipart, then meros will resolve with the
response argument.
Example on how to handle this case
import { meros } from 'meros';
const response = await fetch('/api');
const parts = await meros(response);
if (parts[Symbol.asyncIterator] < 'u') {
for await (const part of parts) {
}
} else {
const data = await parts.json();
}
each Part
gives you access to:
json: boolean
~ Tells you the body
would be a JavaScript object of your
defined generic T
.headers: object
~ A key-value pair of all headers discovered from this part.body: T | Fallback
~ Is the body of the part, either as a JavaScript
object (noted by json
) or the base type of the environment
(Buffer | string
, for Node and Browser respectively).
options.multiple: boolean
Default: false
Setting this to true
will yield once for all available parts of a chunk,
rather than yielding once per part. This is an optimization technique for
technologies like GraphQL where rather than commit the payload to the store, to
be added-to in the next process-tick we can simply do that synchronously.
Important: This will alter the behaviour and yield arrays—than yield
payloads.
const chunks = await fetch('/api').then((response) =>
meros(response, { multiple: true }),
);
for await (const parts of chunks) {
for (const part of parts) {
}
}
💨 Benchmark
Validation :: node
✔ meros
✘ it-multipart (FAILED @ "should match reference patch set")
Benchmark :: node
meros x 289,318 ops/sec ±1.21% (81 runs sampled)
it-multipart x 173,136 ops/sec ±0.85% (80 runs sampled)
Validation :: browser
✔ meros
✘ fetch-multipart-graphql (FAILED @ "should match reference patch set")
Benchmark :: browser
meros x 1,000,417 ops/sec ±1.41% (81 runs sampled)
fetch-multipart-graphql x 353,207 ops/sec ±0.92% (83 runs sampled)
Ran with Node v15.8.0
❤ Thanks
Special thanks to Luke Edwards for performance
guidance and high level api design.
License
MIT © Marais Rossouw
Footnote
1: By default, we'll look for JSON, and parse that for you. If not, we'll give
you the body as what was streamed.