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

157 lines
6.9 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { QuasiValue as Q, Dataspace, Ref, Sturdy, Observe, AnyValue, Reader, Embedded, Schemas } from "@syndicate-lang/core";
import { boot as bootHtml, Anchor, template as html, HtmlFragments, GlobalEvent, UIAttribute } from "@syndicate-lang/html";
import { boot as bootWakeDetector, WakeEvent } from "./wake-detector";
import { boot as bootWsRelay, ForceRelayDisconnect, ViaRelay, RelayAddress, fromForceRelayDisconnect, fromViaRelay } from "./wsRelay";
import { fromSays, fromPresent, Present, Says } from './gen/simpleChatProtocol';
const Transport = Schemas.transportAddress;
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);
}
}
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();
Dataspace.boot(ds => {
bootHtml(ds);
bootWakeDetector(ds);
bootWsRelay(ds);
spawnInputChangeMonitor(ds);
spawn named 'chat' {
at ds {
const ui = new Anchor();
field nym: string = '';
on asserted InputValue('#nym', $v: string) => nym.value = v;
// v Ideally, this would be of type "Sturdy.SturdyRef | null", but (1) the
// current Dataflow implementation isn't bright enough to mark valuesas being
// convertible to preserves values on demand, and (2) null isn't preservesable
// in any case. If preserves were improved to be able to convert schema-parsed
// values to preserves on demand, and to know it could do that at the type
// level, then I could change Dataflow to support any value that could be
// converted to preserves on demand, and I could special-case null and
// undefined for the ergonomics.
field servercap: AnyValue = false;
on asserted InputValue('#servercap', $v: string) => {
servercap.value = false;
try {
const a = new Reader<Ref>(v).next();
if (Sturdy.toSturdyRef(a) !== void 0) servercap.value = a;
} catch (e) {
console.error(e);
}
}
assert UIAttribute('#servercap', 'class', 'invalid') when (!servercap.value);
during InputValue('#wsurl', $wsurl: string) => {
const addr = RelayAddress(Transport.WebSocket(wsurl));
assert fromViaRelay(ViaRelay({
"addr": addr,
"assertion": Schemas.gatekeeper.fromResolve(Schemas.gatekeeper.Resolve({
"sturdyref": Sturdy.asSturdyRef(servercap.value),
"observer": create ({
assert(remoteDs_e: Embedded<Ref>) {
const remoteDs = remoteDs_e.embeddedValue;
console.log('Saw remoteDs', remoteDs);
on message WakeEvent() =>
send message fromForceRelayDisconnect(ForceRelayDisconnect(addr));
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));
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);
}
}
}
}),
})),
})) when (wsurl && servercap.value);
}
}
}
});
}
///////////////////////////////////////////////////////////////////////////
// Input control value monitoring
assertion type InputValue(selector, value);
function spawnInputChangeMonitor(ds: Ref) {
spawn {
at ds {
during Observe({
"pattern": :pattern InputValue(\Q.lit($selector: string), \_)
}) => spawn named `input(${selector})` {
const element = document.querySelector(selector) as HTMLInputElement;
if (element !== null) {
field value: string = element.value;
assert InputValue(selector, value.value);
on message GlobalEvent(selector, 'change', $_e) => value.value = element.value;
}
}
}
}
}
///////////////////////////////////////////////////////////////////////////
// Adding items to the transcript panel
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>`);
}