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.
| Type | Value | Server Type | Client Type |
|---|---|---|---|
| Date | 2025-01-15T12:00:00.000Z | Date | |
| RegExp | /hello\s+world/gi | RegExp | |
| Map | Map(3) { a => 1, b => 2, c => 3 } | Map | |
| Set | Set(3) { 10, 20, 30 } | Set | |
| BigInt | 9007199254740993n | BigInt | |
| URL | https://mochi.dev/docs?version=5 | URL | |
| URLSearchParams | theme=dark&lang=en | URLSearchParams | |
| Uint8Array | Uint8Array [72, 101, 108, 108, 111] | Uint8Array | |
| undefined | undefined | undefined | |
| Infinity | Infinity | Infinity | |
| NaN | NaN | NaN | |
| -0 | -0 | -0 | |
| Repeated ref | [{"x":1},{"x":1}] | Array | |
| Cyclic ref | { self: [Circular] } | object | |
| Repeated ref | same ref | identity check | |
| Cyclic ref | self === obj | identity 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>
More demos
Hydration Modes The same component rendered four ways — eager, lazy, visible, and deferred server island. Form Errors A thrown server error shown inline via {@attach enhance(...)}, or as the Mochi error page on plain submit. Hacker News Clone A full Hacker News reader built on Mochi — SSR pages, hydrated islands, real API.