npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

thaler

v0.9.0

Published

Isomorphic server-side functions

Downloads

10

Readme

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: '[email protected]' },
  { name: 'Jane Doe', email: '[email protected]' },
]);

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