Pulse
pulse-client (JS)

Serialization

Wire format for Pulse client-server communication

Serialization

Pulse uses a custom serialization format for WebSocket messages. This format handles JavaScript types that JSON doesn't support natively, like Date, Set, Map, and object references.


serialize

Converts a JavaScript value to the Pulse wire format.

import { serialize } from "pulse-ui-client";

function serialize(data: Serializable): Serialized

Parameters

ParameterTypeDescription
dataSerializableAny serializable value

Returns

type Serialized = [[number[], number[], number[], number[]], PlainJSON];
//                  ^refs     ^dates    ^sets     ^maps     ^payload

A tuple containing:

  1. Metadata arrays tracking special types by index
  2. The JSON payload

Supported Types

TypeHandling
null, undefinedPassed through
boolean, stringPassed through
numberPassed through (NaN becomes null, Infinity throws)
DateConverted to timestamp
ArrayRecursively processed
SetConverted to array
MapConverted to object (string keys only)
ObjectRecursively processed
Circular refsTracked and restored

Example

import { serialize } from "pulse-ui-client";

const data = {
  name: "Alice",
  createdAt: new Date("2024-01-15"),
  tags: new Set(["admin", "active"]),
  metadata: new Map([["version", 1]]),
};

const serialized = serialize(data);
// [[[], [0], [1], [2]], { name: "Alice", createdAt: 1705276800000, tags: ["admin", "active"], metadata: { version: 1 } }]

Circular References

The serializer handles circular references by tracking object indices:

const obj = { name: "root" };
obj.self = obj;  // circular reference

const serialized = serialize(obj);
// The reference is preserved and restored on deserialize

Error Cases

// Infinity throws an error
serialize({ value: Infinity });
// Error: Cannot serialize Infinity in 'value'

// NaN is converted to null (with no error)
serialize({ value: NaN });
// [[[], [], [], []], { value: null }]

deserialize

Converts the Pulse wire format back to JavaScript values.

import { deserialize } from "pulse-ui-client";

function deserialize<T>(payload: Serialized, options?: DeserializationOptions): T

Parameters

ParameterTypeDescription
payloadSerializedWire format data
optionsDeserializationOptionsOptional settings

Options

interface DeserializationOptions {
  coerceNullsToUndefined?: boolean;
}
OptionDefaultDescription
coerceNullsToUndefinedfalseConvert null values to undefined

Returns

The reconstructed JavaScript value with proper types restored.

Example

import { deserialize } from "pulse-ui-client";

const serialized: Serialized = [
  [[], [0], [1], [2]],
  {
    name: "Alice",
    createdAt: 1705276800000,
    tags: ["admin", "active"],
    metadata: { version: 1 },
  },
];

const data = deserialize(serialized);
// {
//   name: "Alice",
//   createdAt: Date("2024-01-15"),  // Date object restored
//   tags: Set(["admin", "active"]), // Set restored
//   metadata: Map([["version", 1]]) // Map restored
// }

Null Coercion

When working with APIs that use undefined for missing values:

const data = deserialize(payload, { coerceNullsToUndefined: true });
// All null values become undefined

Wire Format Details

The serialized format is designed to be compact and handle edge cases that plain JSON cannot.

Format Structure

type Serialized = [
  [
    number[],  // refs: indices of circular references
    number[],  // dates: indices of Date objects
    number[],  // sets: indices of Set objects
    number[],  // maps: indices of Map objects
  ],
  PlainJSON    // The actual data as JSON
];

Index Tracking

During serialization, a global counter increments for each visited node. The metadata arrays record which indices are special types:

// Example: { date: new Date(), items: new Set([1, 2]) }
// Index 0: root object
// Index 1: Date at "date" key
// Index 2: Set at "items" key

// Serialized:
[
  [[], [1], [2], []],  // dates=[1], sets=[2]
  { date: 1705276800000, items: [1, 2] }
]

Circular Reference Resolution

const a = { name: "a" };
const b = { name: "b", ref: a };
a.ref = b;  // circular

// Serialized:
[
  [[3], [], [], []],  // refs=[3] (index 3 is a back-reference)
  { name: "a", ref: { name: "b", ref: 0 } }
  //                              ^ references index 0
]

extractServerRouteInfo

Extracts route information from a React Router loader function. Use this in SSR loaders to pass route context to the Pulse server.

import { extractServerRouteInfo } from "pulse-ui-client";

function extractServerRouteInfo(args: LoaderFunctionArgs): RouteInfo

Parameters

ParameterTypeDescription
argsLoaderFunctionArgsReact Router loader arguments

Returns

interface RouteInfo {
  pathname: string;
  hash: string;
  query: string;
  queryParams: Record<string, string>;
  pathParams: Record<string, string | undefined>;
  catchall: string[];
}

Example

import { extractServerRouteInfo } from "pulse-ui-client";
import type { LoaderFunctionArgs } from "react-router";

export async function loader(args: LoaderFunctionArgs) {
  const routeInfo = extractServerRouteInfo(args);

  // routeInfo.pathname = "/users/123"
  // routeInfo.pathParams = { id: "123" }
  // routeInfo.queryParams = { tab: "settings" }

  // Pass to Pulse server for SSR
  const prerender = await fetchPulsePrerender(routeInfo);
  return { prerender };
}

submitForm

Programmatically submits a form via fetch. This is what PulseForm uses internally.

import { submitForm } from "pulse-ui-client";

async function submitForm(options: SubmitFormOptions): Promise<void>

Parameters

interface SubmitFormOptions {
  event: FormEvent<HTMLFormElement>;
  action: string;
  onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
  formData?: FormData;
  force?: boolean;
}
OptionTypeDescription
eventFormEvent<HTMLFormElement>The form submit event
actionstringForm action URL
onSubmitfunctionOptional submit handler
formDataFormDataOptional pre-built form data
forcebooleanSubmit even if defaultPrevented

Example

import { submitForm } from "pulse-ui-client";

function CustomForm() {
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    // Custom validation
    const form = e.currentTarget;
    if (!form.checkValidity()) {
      e.preventDefault();
      return;
    }

    // Custom form data
    const formData = new FormData(form);
    formData.append("timestamp", Date.now().toString());

    await submitForm({
      event: e,
      action: "/api/submit",
      formData,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit">Submit</button>
    </form>
  );
}

See Also

On this page