Skip to content

lttb/convey

Repository files navigation

convey

This project is still at an early stage and is not production ready. API could be changed.

Key Features

  • Seameless code usage between client and server – call server functions just as normal functions (and vice-versa).
  • Out of the box streaming support (with Server Sent Events by default)
  • Framework agnostic
  • Strong Typescript support
  • Advanced resolver caching options: from HTTP-level to session storage
  • Performant React component subscriptions, and automatic cache invalidation rerendering

Examples

codesandbox example

Quick Start

Install dependencies:

npm install --save @convey/core
npm install --save-dev @convey/babel-plugin

Optional for usage with react:

npm install --save @convey/react

Babel config

See nextjs babel.config.js for example

Add @convey/babel-plugin to your babel config:

// babel.config.js

module.exports = {
    plugins: [
        [
            '@convey',
            {
                /**
                 * Determine "remote" resolvers
                 *
                 * "server" resolvers will be processed as remote for the "client" code, and vice versa
                 */
                remote:
                    process.env.TARGET === 'client'
                        ? /resolvers\/server/
                        : /resolvers\/client/,
            },
        ],
    ],
};

Server handler config

See nextjs pages/api/resolver/[id].ts for example

import {createResolverHandler} from '@convey/core/server';

import * as resolvers from '@app/resolvers/server';

const handleResolver = createResolverHandler(resolvers);

export default async function handle(req, res) {
    await handleResolver(req, res);
}

Client config

See nextjs pages/_app.tsx for example

import {setConfig} from '@convey/core';
import {createResolverFetcher} from '@convey/core/client';

setConfig({
    fetch: createResolverFetcher(),
});

Declare and use resolvers

Server resolver

resolvers/server/index.tsx

import {exec} from 'child_process';
import {promisify} from 'util';

import {createResolver, createResolverStream} from '@convey/core';

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

/**
 * This code will be executed on the server side
 */
export const getServerDate = createResolver(async () =>
    promisify(exec)('date').then((x) => x.stdout.toString())
);

export const getServerHello = createResolver(
    (name: string) => `Hello, ${name}`
);

/**
 * It is also possible to declare the stream via generator function.
 * By default, the data will be streamed by SSE (Server Sent Events)
 */
export const getServerHelloStream = createResolverStream(async function* (
    name: string
) {
    while (true) {
        /**
         * Resolvers could be called as normal functions on server side too
         */
        yield await getServerHello(`${name}-${await getServerDate()}`);
        await wait(1000);
    }
});

After processing, on the client-side the actual code will be like:

import {createResolver, createResolverStream} from '@convey/core';

/**
 * This code will be executed on the server side
 */

export const getServerDate = createResolver(
    {},
    {
        id: '3296945930:getServerDate',
    }
);

export const getServerHello = createResolver(
    {},
    {
        id: '3296945930:getServerHello',
    }
);

/**
 * It is also possible to declare the stream via generator function.
 * By default, the data will be streamed by SSE (Server Sent Events)
 */
export const getServerHelloStream = createResolverStream(
    {},
    {
        id: '3296945930:getServerHelloStream',
    }
);

Client resolver usage

Direct usage:

import {getServerHello, getServerHelloStream} from '@app/resolvers/server';

console.log(await getServerHello('world')); // `Hello, world`

for await (let hello of getServerHelloStream('world')) {
    console.log(hello); // `Hello, world-1637759100546` every second
}

Usage with React:

import {useResolver} from '@convey/react';
import {getServerHello, getServerHelloStream} from '@app/resolvers/server';

export const HelloComponent = () => {
    /**
     * Component will be automatically rerendered on data invalidation
     */
    const [hello] = useResolver(getServerHello('world'));
    /**
     * If resolver is a stream, then component will be rerendered
     * on each new chunk of data
     */
    const [helloStream] = useResolver(getServerHelloStream('world'));

    return (
        <div>
            <p>Single hello: {hello}</p>
            <p>Stream hello: {helloStream}</p>
        </div>
    );
};

Thanks

This project was heavily inspired by work of amazing engineers at Yandex.Market: