thaler
v0.9.0
Published
Isomorphic server-side functions
Downloads
11
Readme
thaler
Isomorphic server-side functions
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 byfn$
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 to250
.
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 at10
ms and doubles every retry, up to the defined maximum interval. The default maximum interval is5000
ms.count
: The maximum number of retries. Default is10
.
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
License
MIT © lxsmnsyc