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

159 lines
6.2 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { fromJS, Bytes, Dataspace, Ref, Sturdy, AnyValue, Reader, Schemas, Embedded, stringify } 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 } from "@syndicate-lang/ws-relay";
import { Present, Says } from './gen/simpleChatProtocol';
import G = Schemas.gatekeeper;
import N = Schemas.noise;
export function main() {
document.getElementById('chat_form')!.onsubmit = e => { e.preventDefault(); return false; };
document.getElementById('nym_form')!.onsubmit = e => { e.preventDefault(); return false; };
setDataspaceAddress();
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('#route', 'value', $routeText: string) => {
let route: G.Route<Ref> | null = null;
try {
route = G.asRoute(new Reader<Ref>(routeText).next());
} catch (e) {
console.error(e);
}
assert UIAttribute('#route', 'class', 'invalid') when (!route);
if (route) contactRemote(route);
}
function contactRemote(route: G.Route<Ref>) {
console.log('contactRemote', route);
during G.ResolvePath({
"route": route,
"addr": $addr,
"control": $control_e: Embedded,
"resolved": G.Resolved.accepted($remoteDs_e: Embedded),
}) => {
const remoteDs = remoteDs_e.embeddedValue;
const control = control_e.embeddedValue;
on message WakeEvent() => at control {
send message G.ForceDisconnect();
}
outputState('connected', 'connected to ' + stringify(addr));
on stop outputState('disconnected', 'disconnected from ' + stringify(addr));
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 Says({ who: nym.value, what: utterance });
}
}
}
at remoteDs {
assert 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 setDataspaceAddress() {
const route = document.getElementById('route')! as HTMLInputElement;
if (route.value === '') {
const isSecure = (document.location.protocol ?? '').toLowerCase().endsWith('s:');
const localWs = isSecure ? 'wss' : 'ws';
const wsurls = [
`wss://generic-dataspace.demo.leastfixedpoint.com/`,
`${localWs}://${document.location.hostname}:9001/`,
];
const transports: AnyValue[] =
wsurls.map(u => fromJS(Schemas.transportAddress.WebSocket(u)));
route.value = stringify(G.Route<Ref>({
"transports": transports,
"pathSteps": [G.PathStep({
"stepType": N.$noise,
"detail": fromJS(N.NoiseSpec({
"service": "syndicate",
"key": Bytes.fromHex("21f6cd4e11e7e37711d6b3084ff18cded8fc8abf293aa47d43e8bb86dda65516"),
"protocol": N.NoiseProtocol.absent(),
"preSharedKeys": N.NoisePreSharedKeys.absent(),
})),
})],
}));
// To use a sturdyref instead:
//
// route.value = stringify(G.Route<Ref>({
// "transports": transports,
// "pathSteps": [G.PathStep({
// "stepType": Sturdy.$ref,
// "detail": fromJS(Sturdy.Parameters({
// "oid": "syndicate",
// "sig": Bytes.fromHex('69ca300c1dbfa08fba692102dd82311a'),
// "caveats": Sturdy.CaveatsField.absent(),
// })),
// })],
// }));
//
// ... and of course you can chain these things, depending on server setup.
}
}
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>`);
}