syndicate-js/examples/example-simple-chat/src/index.ts

126 lines
5.0 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Dataspace, Ref, Sturdy, AnyValue, Reader, Schemas, isEmbedded } from "@syndicate-lang/core";
import { boot as bootHtml, Anchor, template as html, HtmlFragments, GlobalEvent, UIAttribute, UIChangeableProperty } from "@syndicate-lang/html";
import { boot as bootWakeDetector, WakeEvent } from "./wake-detector";
import { boot as bootWsRelay, ForceRelayDisconnect, fromRelayAddress, RelayAddress, fromForceRelayDisconnect, Resolved } from "./wsRelay";
import { fromSays, fromPresent, Present, Says } from './gen/simpleChatProtocol';
const Transport = Schemas.transportAddress;
export function main() {
document.getElementById('chat_form')!.onsubmit = e => { e.preventDefault(); return false; };
document.getElementById('nym_form')!.onsubmit = e => { e.preventDefault(); return false; };
setWsurl();
setUsernameIfUnset();
document.getElementById('chat_input')!.focus();
Dataspace.boot(ds => {
bootHtml(ds);
bootWakeDetector(ds);
bootWsRelay(ds, true);
bootChat(ds);
});
}
function bootChat(ds: Ref) {
spawn named 'chat' {
at ds {
field nym: string = '';
on asserted UIChangeableProperty('#nym', 'value', $v: string) => nym.value = v;
during UIChangeableProperty('#wsurl', 'value', $wsurl: string) =>
during UIChangeableProperty('#servercap', 'value', $servercapText: string) => {
let servercap: AnyValue | null = null;
try {
const a = new Reader<Ref>(servercapText).next();
Sturdy.asSturdyRef(a); // throws if invalid
servercap = a;
} catch (e) {
console.error(e);
}
assert UIAttribute('#servercap', 'class', 'invalid') when (!servercap);
if (wsurl && servercap) {
contactRemote(wsurl, servercap);
}
}
function contactRemote(wsurl: string, servercap: AnyValue) {
during Resolved({
"addr": RelayAddress(Transport.WebSocket(wsurl)),
"sturdyref": servercap,
"resolved": $remoteDs_e,
}) => {
if (!isEmbedded(remoteDs_e)) return;
const remoteDs = remoteDs_e.embeddedValue;
on message WakeEvent() =>
send message fromForceRelayDisconnect(ForceRelayDisconnect(
RelayAddress(Transport.WebSocket(wsurl))));
outputState('connected', 'connected to ' + wsurl);
on stop outputState('disconnected', 'disconnected from ' + wsurl);
on message GlobalEvent('#send_chat', 'click', _) => {
const inp = document.getElementById("chat_input") as HTMLInputElement;
var utterance = inp.value;
inp.value = '';
if (utterance) {
at remoteDs {
send message fromSays(Says({ who: nym.value, what: utterance }));
}
}
}
at remoteDs {
assert fromPresent(Present(nym.value));
const ui = new Anchor();
during Present($who: string) => at ds {
assert ui.context(who).html('#nymlist', html`<li>${who}</li>`);
}
on message Says({ "who": $who: string, "what": $what: string }) => {
outputUtterance(who, what);
}
}
}
}
}
}
}
function setWsurl() {
const wsurl = document.getElementById('wsurl')! as HTMLInputElement;
if (wsurl.value === '') {
wsurl.value = `ws://${document.location.host}:9001/`;
}
}
function setUsernameIfUnset() {
const nym = document.getElementById('nym')! as HTMLInputElement;
if (nym.value === '') {
nym.value = "nym" + Math.floor(Math.random() * 65536);
}
}
function outputItem(cls: string, item0: HtmlFragments): ChildNode {
const stamp = html`<span class="timestamp">${(new Date()).toUTCString()}</span>`;
const item = html`<div class="${cls}">${stamp}${item0}</div>`;
const n = item.node();
const o = document.getElementById('chat_output')!;
o.appendChild(n);
o.scrollTop = o.scrollHeight;
return n;
}
function outputState(cls: string, state: string) {
outputItem(`state_${cls}`, html`<span class="state ${cls}">${state}</span>`)
}
function outputUtterance(who: string, what: string) {
outputItem('utterance',
html`<span class="nym">${who}</span><span class="utterance">${what}</span>`);
}