Git Product home page Git Product logo

thaler's Introduction

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

thaler's People

Contributors

lxsmnsyc avatar marbemac avatar

Stargazers

Dmitriy O. avatar Nikola Hristov avatar Muhammad Amin Saffari Taheri avatar Okiki Ojo avatar Quinten Coret avatar Joshua Amaju avatar Jacob Groß avatar Katja Lutz avatar Jasmin avatar Jérémy Chatard avatar Iha Shin (Cosmo, 신의하) avatar KarlitosD avatar Jonas Galvez avatar Alexander Mikuta avatar  avatar Ricky Azhari avatar Yuji Sugiura avatar Mark Malstrom avatar Tim avatar Allan Crisostomo avatar Mathias Beugnon avatar KIPRI avatar  avatar Alexander avatar Benj avatar Alexander Ryzhikov avatar Mohammed avatar Papi Peppers avatar Taishi Naritomi avatar Samual Norman avatar Vladislav Lipatov avatar Ryan Conceicao avatar  avatar Alex Benfaremo avatar Patrick G avatar Franklin avatar Alexei Accio avatar  avatar  avatar Dương Tấn Huỳnh Phong avatar Đức An avatar Patryk Szydłowski avatar raph avatar bigmistqke avatar John Wright avatar John Eatmon avatar Julian Cataldo avatar Roman avatar Iain Simmons avatar Soorria Saruva avatar Nikita avatar Rui Duarte avatar  avatar Peter Griffin avatar ElBe2049 avatar James Domingo avatar Prince Neil Cedrick Castro avatar Huy Giang avatar Gabriel Pinheiro avatar David Sancho avatar Andrejs Agejevs avatar Alexandre Stahmer avatar Clifford Fajardo  avatar  avatar Adrian Dayrit avatar

Watchers

 avatar  avatar

Forkers

orjdev

thaler's Issues

Client bundle contains server deps

I got this when use thaler + vite:
CleanShot 2023-04-07 at 23 31 39@2x

After deep dive to the compiler, I noticed that imports for RPC did not remove from client bundle, it just like a import and unused like below
CleanShot 2023-04-07 at 23 33 34@2x

I don't think this is thaler's job (Vite should removes them), but have no idea how to fix it 😿

Passing per request context to callbacks?

Consider the below pseudocode - any ideas re how one might set and then access per request props in the handlers?

The use case for is for things like currentUser, a or a per request db connection object, per request cache, etc.

import { handleRequest } from 'thaler/server';

// Cloudflare worker pseudocode
export default {
  async fetch(request) {
    // build up per request context
    const ctx = {
      // some db that operates over http/ws, such as neon (pg), planetsacle, or clickhouse, etc
      db: initDbCon(),
      currentUser: await parseReqUser(request),
      logger,
    }

    /** 
     * Could pass ctx in for this case, when a call is being made from client? To be made available to the callback handlers
     * 
     * BUT not sure how we could get ctx to the callback handlers in the SSR case, when no new fetch is involved
     */
    const request = await handleRequest(request, { extra: ctx });
    if (request) {
      // Request was matched
      return request;
    }

    // render and return the app html stream
    return new Response(renderToStream(() => <App />))
  },
};


// Example $loader
const list = loader$(async (_, { extra }) => {
  if (!extra.currentUser) {
    // error out, redirect, etc
  }

  return extra.db.listPosts();
});

Possible to skip the fetch calls on the server?

Similar to bling's approach (https://github.com/TanStack/bling).

"The function passed to server$ will only be executed on the server. On the client, a fetch call is made to the server function instead."

Basically in environments like cloudflare it's really nice to skip the http round trips during SSR for this data fetching. This also might introduce extra complexity at the infra level, depending on how the fetch calls are put together - e.g. dealing with a server making calls to itself back through the top level dns, since at least in our applications we explicitly disallow requests out to the public net for security reasons.

Thanks for the work on this library, I've enjoyed digging through the implementation and am pretty excited about where it could go!

Feature: Streaming server functions

Basically the streaming version of the currently existing functions. Arguably the design would look like the following:

// Maybe as a extended function?
fn$.stream(async * () => {
  for (let i = 0; i < 10; i++) {
    yield `Count: ${i}`;
  }
});

Using fn$

Hello,

I'm attempting to use thaler with Astro, and I'm encountering some interesting behavior, I'm not sure if I'm using the library correct or not.

When attempting to use fn$ like so:

let orders = fn$(get_orders)

I get the following error:

Error: fn$ cannot be called during runtime.
    at Module.fn$ (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/thaler/dist/esm/development/index.mjs:47:9)
    at Example (C:\Users\Pglenn\Desktop\amring_web_app\src\components\Example.tsx:24:14)
    at Module.createComponent (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:349:15)
    at __vite_ssr_import_0__.renderToString.renderId.renderId (C:\Users\Pglenn\Desktop\amring_web_app\node_modules\@astrojs\solid-js\dist\server.js:27:14)
    at file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/web/dist/server.js:104:34
    at createRoot (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:58:14)
    at Module.renderToString (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/web/dist/server.js:102:14)
    at Object.renderToStaticMarkup (C:\Users\Pglenn\Desktop\amring_web_app\node_modules\@astrojs\solid-js\dist\server.js:14:16)
    at Object.check (C:\Users\Pglenn\Desktop\amring_web_app\node_modules\@astrojs\solid-js\dist\server.js:7:41)
    at renderFrameworkComponent (C:\Users\Pglenn\Desktop\amring_web_app\node_modules\astro\dist\runtime\server\render\component.js:98:33)
    at async Module.renderComponent (C:\Users\Pglenn\Desktop\amring_web_app\node_modules\astro\dist\runtime\server\render\component.js:346:10)

However when I use it like so there is no error:

let orders = fn$(async () => {
  return await get_orders()
})

Also when I attempt to use fn$ like so:

let resource = afn => {
  let res = fn$(async () => await afn())
  return createAsync(res)
}

I get the following error:

Error: Cannot serialize function without reference ID.
    at X.parseFunction (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:8023)
    at X.parse (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:16731)
    at X.parseItems (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:10593)
    at X.parseArray (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:10662)
    at X.parseObject (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:14063)
    at X.parse (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:16643)
    at X.parseProperties (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:10804)
    at X.parsePlainObject (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:11371)
    at X.parseObject (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:14133)
    at X.parse (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:16643)
    at Un (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/seroval/dist/esm/production/index.mjs:3:44834)
    at serializeFunctionBody (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/thaler/dist/esm/development/server.mjs:33:31)
    at fnHandler (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/thaler/dist/esm/development/server.mjs:210:22)
    at subFetch (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/@solidjs/router/dist/data/createAsync.js:47:16)
    at eval (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/@solidjs/router/dist/data/createAsync.js:11:67)
    at load (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:524:78)
    at Module.createResource (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:573:42)
    at Module.createAsync (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/@solidjs/router/dist/data/createAsync.js:11:46)
    at resource (C:/Users/Pglenn/Desktop/amring_web_app/src/components/Example.tsx:20:34)
    at Example (C:/Users/Pglenn/Desktop/amring_web_app/src/components/Example.tsx:22:16)
    at Module.createComponent (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:349:15)
    at __vite_ssr_import_0__.renderToString.renderId.renderId (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/@astrojs/solid-js/dist/server.js:31:36)
    at file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/web/dist/server.js:104:34
    at createRoot (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/dist/server.js:58:14)
    at Module.renderToString (file:///C:/Users/Pglenn/Desktop/amring_web_app/node_modules/solid-js/web/dist/server.js:102:14)
    at Object.renderToStaticMarkup (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/@astrojs/solid-js/dist/server.js:18:38)
    at renderFrameworkComponent (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/astro/dist/runtime/server/render/component.js:209:66)
    at async Module.renderComponent (C:/Users/Pglenn/Desktop/amring_web_app/node_modules/astro/dist/runtime/server/render/component.js:360:10)

I assume it's possible to use this function in a primitive way like I've attempted, but it doesn't seem possible. Is this by design? If not this might be a bug.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.