🍡 mochi
← All demos

Devalue Serialization

Rich type serialization via devalue. The Server Type column is captured during SSR and shipped as strings; the Client Type column is resolved live after hydration. They should match — proving the types survived the round-trip.

TypeValueServer TypeClient Type
Date2025-01-15T12:00:00.000ZDate
RegExp/hello\s+world/giRegExp
MapMap(3) { a => 1, b => 2, c => 3 }Map
SetSet(3) { 10, 20, 30 }Set
BigInt9007199254740993nBigInt
URLhttps://mochi.dev/docs?version=5URL
URLSearchParamstheme=dark&lang=enURLSearchParams
Uint8ArrayUint8Array [72, 101, 108, 108, 111]Uint8Array
undefinedundefinedundefined
InfinityInfinityInfinity
NaNNaNNaN
-0-0-0
Repeated ref[{"x":1},{"x":1}]Array
Cyclic ref{ self: [Circular] }object
Repeated refsame refidentity check
Cyclic refself === objidentity check

Server (SSR)

<script>
  import { isBrowser } from 'mochi-framework';
  import { typeOf } from './devalueTypeOf.ts';

  let {
    dateVal,
    regexpVal,
    mapVal,
    setVal,
    bigintVal,
    urlVal,
    searchParamsVal,
    typedArrayVal,
    undefinedVal,
    infinityVal,
    nanVal,
    negZeroVal,
    repeatedRef,
    cyclicRef,
    serverTypes,
  } = $props();

  function display(v) {
    if (v === undefined) {
      return 'undefined';
    }
    if (v === null) {
      return 'null';
    }
    if (Number.isNaN(v)) {
      return 'NaN';
    }
    if (v === Infinity) {
      return 'Infinity';
    }
    if (Object.is(v, -0)) {
      return '-0';
    }
    if (typeof v === 'bigint') {
      return `${v}n`;
    }
    if (v instanceof Date) {
      return v.toISOString();
    }
    if (v instanceof RegExp) {
      return String(v);
    }
    if (v instanceof Map) {
      return `Map(${v.size}) { ${[...v.entries()].map(([k, val]) => `${k} => ${val}`).join(', ')} }`;
    }
    if (v instanceof Set) {
      return `Set(${v.size}) { ${[...v].join(', ')} }`;
    }
    if (v instanceof URL) {
      return v.href;
    }
    if (v instanceof URLSearchParams) {
      return v.toString();
    }
    if (v instanceof Uint8Array) {
      return `Uint8Array [${[...v].join(', ')}]`;
    }
    if (Array.isArray(v)) {
      return JSON.stringify(v);
    }
    if (typeof v === 'object' && v !== null) {
      if (v.self === v) {
        return '{ self: [Circular] }';
      }
      return JSON.stringify(v);
    }
    return String(v);
  }

  // svelte-ignore state_referenced_locally
  const rows = [
    { label: 'Date', value: dateVal },
    { label: 'RegExp', value: regexpVal },
    { label: 'Map', value: mapVal },
    { label: 'Set', value: setVal },
    { label: 'BigInt', value: bigintVal },
    { label: 'URL', value: urlVal },
    { label: 'URLSearchParams', value: searchParamsVal },
    { label: 'Uint8Array', value: typedArrayVal },
    { label: 'undefined', value: undefinedVal },
    { label: 'Infinity', value: infinityVal },
    { label: 'NaN', value: nanVal },
    { label: '-0', value: negZeroVal },
    { label: 'Repeated ref', value: repeatedRef },
    { label: 'Cyclic ref', value: cyclicRef },
  ];
</script>

<div class="devalue-demo">
  <table>
    <thead>
      <tr>
        <th>Type</th>
        <th>Value</th>
        <th>Server Type</th>
        <th>Client Type</th>
      </tr>
    </thead>
    <tbody>
      {#each rows as row (row.label)}
        {@const clientType = isBrowser ? typeOf(row.value) : ''}
        {@const serverType = serverTypes?.[row.label] ?? '—'}
        <tr>
          <td class="label">{row.label}</td>
          <td class="value"><code>{display(row.value)}</code></td>
          <td class="type">{serverType}</td>
          <td class="type pending">{clientType}</td>
        </tr>
      {/each}
      <tr>
        <td class="label">Repeated ref</td>
        <td class="value"><code>{repeatedRef?.[0] === repeatedRef?.[1] ? 'same ref' : 'different refs'}</code></td>
        <td class="type">identity check</td>
        <td class="type pending">{isBrowser ? 'identity check' : ''}</td>
      </tr>
      <tr>
        <td class="label">Cyclic ref</td>
        <td class="value"><code>{cyclicRef?.self === cyclicRef ? 'self === obj' : 'broken'}</code></td>
        <td class="type">identity check</td>
        <td class="type pending">{isBrowser ? 'identity check' : ''}</td>
      </tr>
    </tbody>
  </table>
  <p class="env">{isBrowser ? 'Client (hydrated)' : 'Server (SSR)'}</p>
</div>
Styles
<style>
  .devalue-demo {
    background: var(--surface-muted);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 0.85rem 1rem;
    overflow-x: auto;
  }

  table {
    width: 100%;
    min-width: 520px;
    border-collapse: collapse;
    font-size: 0.9rem;
  }

  th {
    text-align: left;
    padding: 0.4rem 0.55rem;
    border-bottom: 2px solid var(--border-strong);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }

  td {
    padding: 0.4rem 0.55rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
  }

  tbody tr:last-child td {
    border-bottom: 0;
  }

  .label {
    font-weight: 600;
    color: var(--text);
  }

  .value code {
    background: var(--surface);
    border: 1px solid var(--border);
    padding: 0.12rem 0.4rem;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--text);
    word-break: break-all;
  }

  .type {
    color: var(--text-subtle);
    font-family: var(--font-mono);
    font-size: 0.85rem;
  }

  .type.pending:empty::before {
    content: '—';
    color: var(--text-subtle);
    opacity: 0.5;
  }

  .env {
    margin-top: 0.85rem;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: 0.95rem;
    color: var(--text-muted);
    text-align: right;
  }
</style>