Devtools

Pure Functions

Learn about pure functions — registerPureFnFactory for server-to-client sharing, and pureServerFn/mapFrom for client-to-server injection.

mion provides a system for defining pure functions that can be safely transmitted over the network between server and client. All pure functions must follow strict purity rules so they can be serialized, transmitted, and executed safely on either side.

Overview

There are three main APIs for working with pure functions, each with a different direction of flow:

APIDefined inDirectionPurpose
registerPureFnFactory()Server codeServer → ClientRegister a utility function on the server that can be shared with clients over the network
pureServerFn()Client codeClient → ServerDefine a function in client code that gets extracted and compiled into the server at build time
mapFrom()Client codeClient → ServerMap the output of one route to the input of another within a routesFlow — compiled into server at build time

All three follow the same purity rules and are processed by the mion Vite plugin at build time.

registerPureFnFactory (Server → Client)

registerPureFnFactory() is used in server code to register pure utility functions that can be shared with clients over the network. These are typically used by mion's JIT compilation system — for example, validation helpers, format functions, or computation utilities that both server and client need.

The factory function receives jitUtils as its single parameter, giving it access to other registered pure functions. The factory is called once at initialization time to create the actual function:

// Register a validation utility on the server
registerPureFnFactory('myNamespace', 'limitItems', function (jitUtils) {
    const MAX_ITEMS = 100;
    return function inner(items: any[]) {
        return items.slice(0, MAX_ITEMS);
    };
});

// Register a factory that depends on another pure function
registerPureFnFactory('myNamespace', 'validateAndLimit', function (jitUtils) {
    const limitFn = jitUtils.getPureFn('myNamespace', 'limitItems');
    return function inner(items: any[]) {
        const filtered = items.filter((item) => item != null);
        return limitFn(filtered);
    };
});

Because these functions are pure (no closures, no side effects, no forbidden globals), they can be safely serialized and sent to clients when needed — for example, when a client requests JIT functions for validation or serialization.

pureServerFn (Client → Server)

pureServerFn() is used in client code to define functions that will be extracted at build time and compiled into the server. This allows clients to define logic that runs safely on the server without the server ever executing arbitrary client code.

At runtime on the client, pureServerFn() returns a lightweight PureServerFnRef — a reference containing the function's hash identifier that the client sends to the server.

// Simple pure function — defined in client, runs on server
const double = pureServerFn((x: number) => x * 2);

// Named pure function
const compute = pureServerFn(function addOne(x: number) {
    const result = x + 1;
    return result;
});

// Factory variant — called once at server init to create the actual function
const validate = pureServerFn({
    pureFn: function factory() {
        const regex = new RegExp('^[a-z]+$');
        return function inner(s: string) {
            return regex.test(s);
        };
    },
    isFactory: true,
});

How pureServerFn Works at Build Time

  1. The Vite plugin scans the client source for pureServerFn() calls
  2. Each function body is extracted via AST analysis and validated against purity rules
  3. The body is normalized and hashed (SHA256) to produce a unique bodyHash
  4. The bodyHash is injected as an additional argument via a TypeScript transformer
  5. All extracted functions are bundled into the virtual:mion-server-pure-fns virtual module
  6. The server imports this virtual module, making the functions available by hash lookup

At runtime, when the client sends a request referencing a pure function by its hash, the server looks it up in the virtual module and executes the server-side copy — never running arbitrary client code.

mapFrom (Client → Server)

mapFrom() is used in client code within a routesFlow to map the output of one route to the input of another. Like pureServerFn, the mapper function is defined in the client but extracted and compiled into the server at build time. This enables GraphQL-style data composition without extra round trips.

const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

async function getOrderWithUser() {
    const order = routes.orders.getById('ORDER-123');
    // mapFrom maps order.userId → users.getById input (runs server-side)
    const mapping = mapFrom(order, (o) => o!.userId);
    // fake() returns a typed placeholder that satisfies the TypeScript compiler
    const user = routes.users.getById(mapping.type());

    const [[orderData, userData]] = await routesFlow([order, user]);
    console.log(`Order by ${userData?.name}`);
}

The mapping.fake() call returns a typed placeholder that satisfies the TypeScript compiler, while at runtime the server uses the mapper function to extract the actual value from the order result.

Valid Examples

Pure functions with params, local variables, and allowed globals:

// Pure function using only params and allowed globals
pureServerFn((x: number) => Math.floor(x * 2));

// Pure function with local variables
pureServerFn(function compute(x: number) {
    const y = x * 2;
    const z = y + 1;
    return z;
});

// Pure function with nested callbacks and destructuring
pureServerFn((items: number[]) => items.map((x) => x + 1).filter((x) => x > 0));
pureServerFn(({a, b}: {a: number; b: number}) => a + b);

Using allowed globals (JSON, Math, RegExp, console, etc.):

// Pure functions can use allowed globals: JSON, Math, RegExp, console, etc.
pureServerFn(function validate(s: string) {
    const encoded = encodeURIComponent(s);
    const num = parseInt(s, 10);
    const valid = isNaN(num) ? false : isFinite(num);
    const regex = new RegExp('^[a-z]+$');
    console.log(encoded, valid, regex);
    return num;
});

// Pure function with try-catch
pureServerFn(function safeParse(s: string) {
    try {
        return JSON.parse(s);
    } catch (e) {
        return null;
    }
});

Factory functions — same purity rules, local scope only:

// Factory functions follow the same purity rules — only local scope is allowed
pureServerFn({
    pureFn: function factory() {
        const regexp = new RegExp('^[a-z]+$');
        return function inner(s: string) {
            return regexp.test(s);
        };
    },
    isFactory: true,
});

// registerPureFnFactory — local variables within the factory are allowed
registerPureFnFactory('ns', 'limitItems', function () {
    const MAX_ITEMS = 100;
    return function inner(items: any[]) {
        return items.slice(0, MAX_ITEMS);
    };
});

mapFrom — valid mappers:

// mapFrom() mapper follows the same purity rules as pureServerFn
const someSubRequest = {} as any; // placeholder for SubRequest
mapFrom(someSubRequest, (value: number) => value * 2);
mapFrom(someSubRequest, (user: {name: string; age: number}) => user.name.toUpperCase());
mapFrom(someSubRequest, function extractId(item: {id: number}) {
    return item.id;
});

Invalid Examples

Impure operations (this, await, dynamic import):

// Using 'this' keyword
pureServerFn(function () {
    return this.x; // ❌ Error: 'this' is not allowed in pure functions
});

// Using async/await
pureServerFn(async (x: number) => await x); // ❌ Error: async/await is not allowed

// Using dynamic import()
pureServerFn((x: string) => import(x)); // ❌ Error: Dynamic import() is not allowed

Forbidden identifiers (eval, fetch, setTimeout, process, window):

// Forbidden identifiers: eval, fetch, setTimeout, process, window, etc.
pureServerFn((x: string) => eval(x)); // ❌ Error: "eval" is not allowed
pureServerFn((url: string) => fetch(url)); // ❌ Error: "fetch" is not allowed
pureServerFn(() => {
    setTimeout(() => {}, 100); // ❌ Error: "setTimeout" is not allowed
});
pureServerFn(() => process.env.KEY); // ❌ Error: "process" is not allowed
pureServerFn(() => window.location.href); // ❌ Error: "window" is not allowed

Closure variables — referencing outer scope:

// Closure variables — referencing outer scope is not allowed
const SECRET = 'my-secret-key';
pureServerFn((x: string) => x + SECRET); // ❌ Error: Closure variable "SECRET" is not allowed

const config = {maxRetries: 3};
pureServerFn((x: number) => x + config.maxRetries); // ❌ Error: Closure variable "config" is not allowed

Imported or dynamic arguments — cannot be statically analyzed:

// Imported functions cannot be statically analyzed — must be defined in the same file
import {myFn} from './helpers.ts';
pureServerFn(myFn); // ❌ Error: argument "myFn" is imported from another module

pureServerFn({pureFn: myFn}); // ❌ Error: argument "myFn" is imported from another module

import {myFactory} from './helpers.ts';
registerPureFnFactory('ns', 'fn', myFactory); // ❌ Error: argument "myFactory" is imported from another module

Invalid mapFrom mappers:

// mapFrom() mapper follows the same purity rules — closure variables are not allowed
const MULTIPLIER = 5;
mapFrom(someSubRequest, (x: number) => x * MULTIPLIER); // ❌ Error: Closure variable "MULTIPLIER" is not allowed

// Forbidden identifiers not allowed in mapFrom() mapper
mapFrom(someSubRequest, (url: string) => fetch(url)); // ❌ Error: "fetch" is not allowed

// Imported mapper not allowed — must be defined inline
import {myMapper} from './helpers.ts';
mapFrom(someSubRequest, myMapper); // ❌ Error: argument "myMapper" is imported from another module

ESLint Integration

The @mionkit/pure-functions ESLint rule validates purity at lint time, catching violations before they reach the build step. See the ESLint Rules page for details.