thaler
TypeScript icon, indicating that this package has built-in type declarations

0.9.0 • Public • Published

thaler

Isomorphic server-side functions

NPM JavaScript Style Guide

Install

npm i thaler
yarn add thaler
pnpm add thaler

What?

thaler allows you to produce isomorphic functions that only runs on the server-side. This is usually ideal if you want to do server-side operations (e.g. database, files, etc.) on the client-side but without adding more abstractions such as defining extra REST endpoints, creating client-side utilities to communicate with the exact endpoint etc.

Another biggest benefit of this is that, not only it is great for isomorphic fullstack apps (i.e. metaframeworks like NextJS, SolidStart, etc.), if you're using TypeScript, type inference is also consistent too, so no need for extra work to manually wire-in types for both server and client.

Examples

Functions

server$

server$ is the simplest of the thaler functions, it receives a callback for processing server Request and returns a Response.

The returned function can then accept request options (which is the second parameter for the Request object), you can also check out fetch

import { server$ } from 'thaler';

const getMessage = server$(async (request) => {
  const { greeting, receiver } = await request.json();

  return new Response(`${greeting}, ${receiver}!`, {
    status: 200,
  });
});

// Usage
const response = await getMessage({
  method: 'POST',
  body: JSON.stringify({
    greeting: 'Hello',
    receiver: 'World',
  }),
});

console.log(await response.text()); // Hello, World!

get$

Similar to server$ except that it can receive an object that can be converted into query params. The object can have a string or an array of strings as its values.

Only get$ can accept search parameters and uses the GET method, which makes it great for creating server-side logic that utilizes caching.

import { get$ } from 'thaler';

const getMessage = get$(async ({ greeting, receiver }) => {
  return new Response(`${greeting}, ${receiver}!`, {
    status: 200,
  });
});

// Usage
const response = await getMessage({
  greeting: 'Hello',
  receiver: 'World',
});

console.log(await response.text()); // Hello, World!

You can also pass some request configuration (same as server$) as the second parameter for the function, however get$ cannot have method or body. The callback in get$ can also receive the Request instance as the second parameter.

import { get$ } from 'thaler';

const getUser = get$((search, { request }) => {
  // do stuff
});

const user = await getUser(search, {
  headers: {
    // do some header stuff
  },
});

post$

If get$ is for GET, post$ is for POST. Instead of query parameters, the object it receives is converted into form data, so the object can accept not only a string or an array of strings, but also a Blob, a File, or an array of either of those types.

Only post$ can accept form data and uses the POST method, which makes it great for creating server-side logic when building forms.

import { post$ } from 'thaler';

const addMessage = post$(async ({ greeting, receiver }) => {
  await db.messages.insert({ greeting, receiver });
  return new Response(null, {
    status: 200,
  });
});

// Usage
await addMessage({
  greeting: 'Hello',
  receiver: 'World',
});

You can also pass some request configuration (same as server$) as the second parameter for the function, however post$ cannot have method or body. The callback in post$ can also receive the Request instance as the second parameter.

import { post$ } from 'thaler';

const addMessage = post$((formData, { request }) => {
  // do stuff
});

await addMessage(formData, {
  headers: {
    // do some header stuff
  },
});

fn$ and pure$

Unlike get$ and post$, fn$ and pure$ uses a superior form of serialization, so that not only it supports valid JSON values, it supports an extended range of JS values.

import { fn$ } from 'thaler';

const addUsers = fn$(async (users) => {
  const db = await import('./db');
  return Promise.all(users.map((user) => db.users.insert(user)));
});

await addUsers([
  { name: 'John Doe', email: 'john.doe@johndoe.com' },
  { name: 'Jane Doe', email: 'jane.doe@janedoe.com' },
]);

You can also pass some request configuration (same as server$) as the second parameter for the function, however fn$ cannot have method or body. The callback in fn$ can also receive the Request instance as the second parameter.

import { fn$ } from 'thaler';

const addMessage = fn$((data, { request }) => {
  // do stuff
});

await addMessage(data, {
  headers: {
    // do some header stuff
  },
});

loader$ and action$

loader$ and action$ is like both get$ and post$ in the exception that loader$ and action$ can return any serializable value instead of Response, much like fn$ and pure$

import { action$, loader$ } from 'thaler';

const addMessage = action$(async ({ greeting, receiver }) => {
  await db.messages.insert({ greeting, receiver });
});

const getMessage = loader$(({ id }) => (
  db.messages.select(id)
));

Closure Extraction

Other functions can capture server-side scope but unlike the other functions (including pure$), fn$ has a special behavior: it can capture the client-side closure of where the function is declared on the client, serialize the captured closure and send it to the server.

import { fn$ } from 'thaler';

const prefix = 'Message:';

const getMessage = fn$(({ greeting, receiver }) => {
  // `prefix` is captured and sent to the server
  return `${prefix} "${greeting}, ${receiver}!"`;
});

console.log(await getMessage({ greeting: 'Hello', receiver: 'World' })); // Message: "Hello, World!"

Note fn$ can only capture local scope, and not global scope. fn$ will ignore top-level scopes.

Warning Be careful on capturing scopes, as the captured variables must only be the values that can be serialized by fn$. If you're using a value that can't be serialized inside the callback that is declared outside, it cannot be captured by fn$ and will lead to runtime errors.

Modifying Response

fn$, pure$, loader$ and action$ doesn't return Response unlike server$, get$ and post$, so there's no way to directly provide more Response information like headers.

As a workaround, these functions receive a response object alongside request.

import { loader$ } from 'thaler';

const getMessage = loader$(({ greeting, receiver }, { response }) => {
  response.headers.set('Cache-Control', 'max-age=86400');
  return `"${greeting}, ${receiver}!"`;
});

Server Handler

To manage the server functions, thaler/server provides a function call handleRequest. This manages all the incoming client requests, which includes matching and running their respective server-side functions.

import { handleRequest } from 'thaler/server';

const request = await handleRequest(request);
if (request) {
  // Request was matched
  return request;
}
// Do other stuff

Your server runtime must have the following Web API:

Some polyfill recommendations:

Intercepting Client Requests

thaler/client provides interceptRequest to intercept/transform outgoing requests made by the functions. This is useful for adding request fields like headers.

import { interceptRequest } from 'thaler/client';

interceptRequest((request) => {
  return new Request(request, {
    headers: {
      'Authorization': 'Bearer <token>',
    },
  });
});

Custom Server Functions

Thaler allows you to define your own server functions. Custom server functions must call one of thaler's internally defined server functions (e.g. $$server from thaler/server and thaler/client) and it has to be defined through the functions config and has the following interface:

// This is based on the unplugin integration
thaler.vite({
  functions: [
    {
      // Name of the function
      name: 'server$',
      // Boolean check if the function needs to perform
      // closure extraction
      scoping: false,
      // Target identifier (to be compiled)
      target: {
        // Name of the identifier
        name: 'server$',
        // Where it is imported
        source: 'thaler',
        // Kind of import (named or default)
        kind: 'named',
      },
      // Compiled function for the client
      client: {
        // Compiled function identifier
        name: '$$server',
        // Where it is imported
        source: 'thaler/client',
        // Kind of import
        kind: 'named',
      },
      // Compiled function for the server
      server: {
        // Compiled function identifier
        name: '$$server',
        // Where it is imported
        source: 'thaler/server',
        // Kind of import
        kind: 'named',
      },
    }
  ],
});

thaler/utils

json(data, responseInit)

A shortcut function to create a Response object with JSON body.

text(data, responseInit)

A shortcut function to create a Response object with text body.

debounce(handler, options)

Creates a debounced version of the async handler. A debounced handler will defer its function call until no calls have been made within the given timeframe.

Options:

  • key: Required. A function that produces a unique string based on the arguments. This is used to map the function call to its timer.
  • timeout: How long (in milliseconds) before a debounce call goes through. Defaults to 250.

throttle(handler, options)

Creates a throttled version of the async handler. A throttled handler calls once for every given timeframe.

Options:

  • key: Required. A function that produces a unique string based on the arguments. This is used to map the function call to its timer.

retry(handler, options)

Retries the handler when it throws an error until it resolves or the max retry count has been reached (in which case it will throw the final error). retry utilizes an exponential backoff process to gradually slow down the retry intervals.

  • interval: The maximum interval for the exponential backoff. Initial interval starts at 10 ms and doubles every retry, up to the defined maximum interval. The default maximum interval is 5000 ms.
  • count: The maximum number of retries. Default is 10.

timeout(handler, ms)

Attaches a timeout to the handler, that will throw if the handler fails to resolve before the given time.

Integrations

Inspirations

Sponsors

Sponsors

License

MIT © lxsmnsyc

Readme

Keywords

Package Sidebar

Install

npm i thaler

Weekly Downloads

3

Version

0.9.0

License

MIT

Unpacked Size

410 kB

Total Files

67

Last publish

Collaborators

  • lxsmnsyc