Below is a two-party channel with a fixed protocol: the Client must first send an integer, then receive a string back. The Server must first receive an integer, then send a string back. Their types are dual — a perfect match.
<div class="panel">
<h3 class="panel-title">Session Type: the protocol contract</h3>
<div class="type-box">
<div class="party">
<span class="party-label client-label">Client</span>
<code class="type-sig">!int . ?string . end</code>
</div>
<div class="dual-arrow" title="Dual types: one's send is the other's receive">⇆</div>
<div class="party">
<span class="party-label server-label">Server</span>
<code class="type-sig">?int . !string . end</code>
</div>
</div>
<p class="hint-text">! = send ? = receive . = then end = done. The types are dual: Client sends what Server receives, and vice versa. A type mismatch is a compile error — the channel never even opens.</p>
</div>
<div class="panel">
<h3 class="panel-title">Live channel</h3>
<div id="channel-vis" class="channel-vis">
<div class="endpoint" id="ep-client">
<div class="ep-label">Client</div>
<div class="ep-state" id="state-client">must send int</div>
</div>
<div class="wire">
<div id="msg-bubble" class="msg-bubble hidden"></div>
</div>
<div class="endpoint" id="ep-server">
<div class="ep-label">Server</div>
<div class="ep-state" id="state-server">must recv int</div>
</div>
</div>
<div id="log" class="log"></div>
<div class="btns">
<button id="btn-correct" type="button">Send correct message</button>
<button id="btn-wrong" type="button" class="danger">Send wrong type</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
/* reset box model */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; padding: .5rem; }
/* card panels */
.panel { background: #f4f7fa; border: 1px solid #dde4eb; border-radius: 10px; padding: 1rem; margin-bottom: .8rem; }
.panel-title { font-size: .88rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
color: #5a7088; margin-bottom: .7rem; }
/* session-type signature row */
.type-box { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.party { display: flex; flex-direction: column; align-items: center; gap: .3rem; flex: 1; min-width: 120px; }
.party-label { font-size: .8rem; font-weight: 700; padding: .15rem .5rem; border-radius: 4px; }
.client-label { background: #dbeafe; color: #1e40af; }
.server-label { background: #dcfce7; color: #166534; }
.type-sig { font-size: .82rem; background: #fff; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .3rem .5rem; white-space: nowrap; }
.dual-arrow { font-size: 1.5rem; color: #94a3b8; flex-shrink: 0; }
.hint-text { font-size: .82rem; color: #64748b; margin-top: .6rem; line-height: 1.4; }
/* animated channel visualization */
.channel-vis { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.endpoint { flex: 1; background: #fff; border: 2px solid #cbd5e1; border-radius: 8px; padding: .5rem;
text-align: center; transition: border-color .3s; }
.endpoint.active { border-color: #3b82f6; }
.endpoint.error { border-color: #ef4444; }
.ep-label { font-size: .78rem; font-weight: 700; color: #475569; margin-bottom: .25rem; }
.ep-state { font-size: .72rem; color: #64748b; font-family: ui-monospace, monospace; }
.wire { flex: 1; height: 2px; background: #cbd5e1; position: relative; display: flex;
align-items: center; justify-content: center; }
.msg-bubble { position: absolute; background: #3b82f6; color: #fff; font-size: .72rem;
font-weight: 700; padding: .2rem .45rem; border-radius: 999px; white-space: nowrap;
transition: opacity .4s; }
.msg-bubble.error { background: #ef4444; }
.msg-bubble.hidden { opacity: 0; }
/* event log */
.log { font-size: .82rem; font-family: ui-monospace, monospace; min-height: 2.4em;
line-height: 1.6; margin-bottom: .6rem; }
.log .ok { color: #15803d; }
.log .err { color: #dc2626; font-weight: 700; }
.log .info { color: #475569; }
/* action buttons */
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; transition: opacity .15s; }
button.danger { background: #dc2626; border-color: #dc2626; }
button.ghost { background: #fff; color: #1d3557; }
// Session-types demo: a binary channel with a fixed two-step protocol.
// Protocol: Client sends int, Server sends string back, then both end.
// Duality: Client type = !int.?string.end, Server type = ?int.!string.end.
// The three protocol steps, in order.
const STEPS = ['send_int', 'recv_string', 'end'];
let step = 0; // current step index (0 = send int, 1 = recv string, 2 = end)
// Human-readable state label for each endpoint at each step.
const STATE_LABELS = {
send_int: { client: 'must send int', server: 'must recv int' },
recv_string: { client: 'must recv string', server: 'must send string' },
end: { client: 'done', server: 'done' }
};
function updateStates() {
const s = STEPS[step] || 'end';
document.getElementById('state-client').textContent = STATE_LABELS[s].client;
document.getElementById('state-server').textContent = STATE_LABELS[s].server;
}
function addLog(text, cls) {
const log = document.getElementById('log');
const line = document.createElement('div');
line.className = cls;
line.textContent = text;
log.appendChild(line);
log.scrollTop = log.scrollHeight;
}
function clearLog() { document.getElementById('log').innerHTML = ''; }
function flashBubble(text, isError) {
const b = document.getElementById('msg-bubble');
b.textContent = text;
b.className = 'msg-bubble' + (isError ? ' error' : '');
void b.offsetWidth; // force reflow so the CSS transition fires again
setTimeout(() => { b.classList.add('hidden'); }, 900);
}
function setEndpointState(id, state) {
const el = document.getElementById(id);
el.classList.remove('active', 'error');
if (state) el.classList.add(state);
}
document.getElementById('btn-correct').addEventListener('click', () => {
if (step === 0) {
// Step 0: Client sends int, Server receives int.
flashBubble('42 (int)', false);
setEndpointState('ep-client', 'active');
setEndpointState('ep-server', 'active');
addLog('✓ Client sent 42 (int). Server received. Protocol advances.', 'ok');
step = 1;
} else if (step === 1) {
// Step 1: Server sends string, Client receives string.
flashBubble('"hello" (string)', false);
setEndpointState('ep-client', 'active');
setEndpointState('ep-server', 'active');
addLog('✓ Server sent "hello" (string). Client received. Protocol complete.', 'ok');
step = 2;
} else {
// Step 2: protocol already complete.
addLog('→ Protocol already completed. Reset to start again.', 'info');
}
updateStates();
});
document.getElementById('btn-wrong').addEventListener('click', () => {
if (step === 0) {
// Wrong type at step 0: client sends string instead of int.
flashBubble('"hello" (string)', true);
setEndpointState('ep-client', 'error');
setEndpointState('ep-server', 'error');
addLog('✗ Client tried to send a string, but the channel expects int at this step.', 'err');
addLog(" " + 'Type error: expected !int on Client side. Channel rejected.', 'err');
} else if (step === 1) {
// Wrong type at step 1: server sends int instead of string.
flashBubble('42 (int)', true);
setEndpointState('ep-client', 'error');
setEndpointState('ep-server', 'error');
addLog('✗ Server tried to send an int, but the channel expects string at this step.', 'err');
addLog(" " + 'Type error: expected !string on Server side. Channel rejected.', 'err');
} else {
addLog('✗ Protocol has ended. No more messages can be sent.', 'err');
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
clearLog();
setEndpointState('ep-client', '');
setEndpointState('ep-server', '');
document.getElementById('msg-bubble').className = 'msg-bubble hidden';
updateStates();
addLog('→ Channel reset. Protocol restarted.', 'info');
});
// Initialize the display.
updateStates();
addLog('→ Channel open. Follow the protocol above.', 'info');
<div class="panel">
<h3 class="panel-title">Session Type: the protocol contract</h3>
<div class="type-box">
<div class="party">
<span class="party-label client-label">Client</span>
<code class="type-sig">!int . ?string . end</code>
</div>
<div class="dual-arrow" title="Dual types: one's send is the other's receive">⇆</div>
<div class="party">
<span class="party-label server-label">Server</span>
<code class="type-sig">?int . !string . end</code>
</div>
</div>
<p class="hint-text">! = send ? = receive . = then end = done. The types are dual: Client sends what Server receives, and vice versa. A type mismatch is a compile error — the channel never even opens.</p>
</div>
<div class="panel">
<h3 class="panel-title">Live channel</h3>
<div id="channel-vis" class="channel-vis">
<div class="endpoint" id="ep-client">
<div class="ep-label">Client</div>
<div class="ep-state" id="state-client">must send int</div>
</div>
<div class="wire">
<div id="msg-bubble" class="msg-bubble hidden"></div>
</div>
<div class="endpoint" id="ep-server">
<div class="ep-label">Server</div>
<div class="ep-state" id="state-server">must recv int</div>
</div>
</div>
<div id="log" class="log"></div>
<div class="btns">
<button id="btn-correct" type="button">Send correct message</button>
<button id="btn-wrong" type="button" class="danger">Send wrong type</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
/* reset box model */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; padding: .5rem; }
/* card panels */
.panel { background: #f4f7fa; border: 1px solid #dde4eb; border-radius: 10px; padding: 1rem; margin-bottom: .8rem; }
.panel-title { font-size: .88rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
color: #5a7088; margin-bottom: .7rem; }
/* session-type signature row */
.type-box { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.party { display: flex; flex-direction: column; align-items: center; gap: .3rem; flex: 1; min-width: 120px; }
.party-label { font-size: .8rem; font-weight: 700; padding: .15rem .5rem; border-radius: 4px; }
.client-label { background: #dbeafe; color: #1e40af; }
.server-label { background: #dcfce7; color: #166534; }
.type-sig { font-size: .82rem; background: #fff; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .3rem .5rem; white-space: nowrap; }
.dual-arrow { font-size: 1.5rem; color: #94a3b8; flex-shrink: 0; }
.hint-text { font-size: .82rem; color: #64748b; margin-top: .6rem; line-height: 1.4; }
/* animated channel visualization */
.channel-vis { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.endpoint { flex: 1; background: #fff; border: 2px solid #cbd5e1; border-radius: 8px; padding: .5rem;
text-align: center; transition: border-color .3s; }
.endpoint.active { border-color: #3b82f6; }
.endpoint.error { border-color: #ef4444; }
.ep-label { font-size: .78rem; font-weight: 700; color: #475569; margin-bottom: .25rem; }
.ep-state { font-size: .72rem; color: #64748b; font-family: ui-monospace, monospace; }
.wire { flex: 1; height: 2px; background: #cbd5e1; position: relative; display: flex;
align-items: center; justify-content: center; }
.msg-bubble { position: absolute; background: #3b82f6; color: #fff; font-size: .72rem;
font-weight: 700; padding: .2rem .45rem; border-radius: 999px; white-space: nowrap;
transition: opacity .4s; }
.msg-bubble.error { background: #ef4444; }
.msg-bubble.hidden { opacity: 0; }
/* event log */
.log { font-size: .82rem; font-family: ui-monospace, monospace; min-height: 2.4em;
line-height: 1.6; margin-bottom: .6rem; }
.log .ok { color: #15803d; }
.log .err { color: #dc2626; font-weight: 700; }
.log .info { color: #475569; }
/* action buttons */
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; transition: opacity .15s; }
button.danger { background: #dc2626; border-color: #dc2626; }
button.ghost { background: #fff; color: #1d3557; }
// Session-types demo: a binary channel with a fixed two-step protocol.
// Protocol: Client sends int, Server sends string back, then both end.
// Duality: Client type = !int.?string.end, Server type = ?int.!string.end.
// The three protocol steps, in order.
const STEPS = ['send_int', 'recv_string', 'end'];
let step = 0; // current step index (0 = send int, 1 = recv string, 2 = end)
// Human-readable state label for each endpoint at each step.
const STATE_LABELS = {
send_int: { client: 'must send int', server: 'must recv int' },
recv_string: { client: 'must recv string', server: 'must send string' },
end: { client: 'done', server: 'done' }
};
function updateStates() {
const s = STEPS[step] || 'end';
document.getElementById('state-client').textContent = STATE_LABELS[s].client;
document.getElementById('state-server').textContent = STATE_LABELS[s].server;
}
function addLog(text, cls) {
const log = document.getElementById('log');
const line = document.createElement('div');
line.className = cls;
line.textContent = text;
log.appendChild(line);
log.scrollTop = log.scrollHeight;
}
function clearLog() { document.getElementById('log').innerHTML = ''; }
function flashBubble(text, isError) {
const b = document.getElementById('msg-bubble');
b.textContent = text;
b.className = 'msg-bubble' + (isError ? ' error' : '');
void b.offsetWidth; // force reflow so the CSS transition fires again
setTimeout(() => { b.classList.add('hidden'); }, 900);
}
function setEndpointState(id, state) {
const el = document.getElementById(id);
el.classList.remove('active', 'error');
if (state) el.classList.add(state);
}
document.getElementById('btn-correct').addEventListener('click', () => {
if (step === 0) {
// Step 0: Client sends int, Server receives int.
flashBubble('42 (int)', false);
setEndpointState('ep-client', 'active');
setEndpointState('ep-server', 'active');
addLog('✓ Client sent 42 (int). Server received. Protocol advances.', 'ok');
step = 1;
} else if (step === 1) {
// Step 1: Server sends string, Client receives string.
flashBubble('"hello" (string)', false);
setEndpointState('ep-client', 'active');
setEndpointState('ep-server', 'active');
addLog('✓ Server sent "hello" (string). Client received. Protocol complete.', 'ok');
step = 2;
} else {
// Step 2: protocol already complete.
addLog('→ Protocol already completed. Reset to start again.', 'info');
}
updateStates();
});
document.getElementById('btn-wrong').addEventListener('click', () => {
if (step === 0) {
// Wrong type at step 0: client sends string instead of int.
flashBubble('"hello" (string)', true);
setEndpointState('ep-client', 'error');
setEndpointState('ep-server', 'error');
addLog('✗ Client tried to send a string, but the channel expects int at this step.', 'err');
addLog(" " + 'Type error: expected !int on Client side. Channel rejected.', 'err');
} else if (step === 1) {
// Wrong type at step 1: server sends int instead of string.
flashBubble('42 (int)', true);
setEndpointState('ep-client', 'error');
setEndpointState('ep-server', 'error');
addLog('✗ Server tried to send an int, but the channel expects string at this step.', 'err');
addLog(" " + 'Type error: expected !string on Server side. Channel rejected.', 'err');
} else {
addLog('✗ Protocol has ended. No more messages can be sent.', 'err');
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
clearLog();
setEndpointState('ep-client', '');
setEndpointState('ep-server', '');
document.getElementById('msg-bubble').className = 'msg-bubble hidden';
updateStates();
addLog('→ Channel reset. Protocol restarted.', 'info');
});
// Initialize the display.
updateStates();
addLog('→ Channel open. Follow the protocol above.', 'info');
Press Send correct message to follow the protocol. Then press Send wrong type — the channel rejects it immediately, just as a type checker would at compile time. Notice that the receiver never has to guard against bad input; the protocol contract did that work upfront.
Comments
Loading comments...