Improve wsRelay

This commit is contained in:
Tony Garnock-Jones 2021-12-13 12:21:29 +01:00
parent 09ae5ddb5b
commit 3fe2af848b
5 changed files with 141 additions and 122 deletions

22
TODO.md
View File

@ -1,3 +1,25 @@
// // 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);
- [DONE] `during/spawn`
- [DONE] `during`
- [DONE] `let { TimeLaterThan } = activate require("@syndicate-lang/driver-timer");`

View File

@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"prepare": "yarn regenerate && yarn compile && yarn rollup",
"regenerate": "rm -rf ./src/gen && preserves-schema-ts --module EntityRef=@syndicate-lang/core --module transportAddress=@syndicate-lang/core:Schemas.transportAddress --output ./src/gen './protocols/schemas/**/*.prs'",
"regenerate": "rm -rf ./src/gen && preserves-schema-ts --module EntityRef=@syndicate-lang/core --module transportAddress=@syndicate-lang/core:Schemas.transportAddress --module sturdy=@syndicate-lang/core:Schemas.sturdy --xref 'node_modules/@syndicate-lang/core/protocols/schemas/**/*.prs' --output ./src/gen './protocols/schemas/**/*.prs'",
"regenerate:watch": "yarn regenerate --watch",
"compile": "syndicate-tsc",
"compile:watch": "syndicate-tsc -w --verbose --intermediate-directory src.ts",

View File

@ -1,5 +1,7 @@
version 1 .
Resolved = <resolved @addr RelayAddress @sturdyref sturdy.SturdyRef @resolved #!any> .
ViaRelay = <via-relay @addr RelayAddress @assertion any> .
ForceRelayDisconnect = <force-relay-disconnect @addr RelayAddress> .

View File

@ -1,14 +1,96 @@
/// 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 { 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, ViaRelay, RelayAddress, fromForceRelayDisconnect, fromViaRelay } from "./wsRelay";
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 === '') {
@ -23,119 +105,6 @@ function setUsernameIfUnset() {
}
}
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>`;

View File

@ -1,22 +1,48 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Ref, Relay, Turn, Supervisor, SupervisorRestartPolicy } from "@syndicate-lang/core";
import { QuasiValue as Q, Ref, Relay, Turn, Supervisor, SupervisorRestartPolicy, Observe, Schemas, assertionFacetObserver, isEmbedded } from "@syndicate-lang/core";
import * as G from "./gen/wsRelay";
export * from "./gen/wsRelay";
export function boot(ds: Ref) {
export function boot(ds: Ref, debug: boolean = false) {
spawn named 'wsRelay' {
at ds {
during Observe({ "pattern": :pattern G.Resolved({
"addr": \Q.lit($addrValue),
"sturdyref": \Q.lit($sturdyRefValue),
"resolved": \Q.bind(),
}) }) => {
const addr = G.toRelayAddress(addrValue);
const sturdyref = Schemas.sturdy.toSturdyRef(sturdyRefValue);
if (addr && sturdyref) {
assert G.fromViaRelay(G.ViaRelay({
"addr": addr,
"assertion": Schemas.gatekeeper.fromResolve(Schemas.gatekeeper.Resolve({
"sturdyref": sturdyref,
"observer": create assertionFacetObserver(e => {
if (isEmbedded(e)) {
assert G.fromResolved(G.Resolved({
"addr": addr,
"sturdyref": sturdyref,
"resolved": e.embeddedValue,
}));
}
}),
})),
}));
}
}
during G.ViaRelay({ "addr": $addrValue }) => spawn named ['wsRelay', addrValue] {
let counter = 0;
new Supervisor({
restartPolicy: SupervisorRestartPolicy.ALWAYS,
}, () => ['wsRelay', addrValue, counter++], () => {
const facet = Turn.activeFacet;
facet.preventInertCheck();
const addr = G.toRelayAddress(addrValue);
if (addr !== void 0) {
stop on message G.ForceRelayDisconnect(addrValue);
const facet = Turn.activeFacet;
const ws = new WebSocket(addr.url);
ws.binaryType = 'arraybuffer';
ws.onclose = () => facet.turn(() => { stop {} });
@ -24,7 +50,7 @@ export function boot(ds: Ref) {
Turn.active.crash(new Error("WebSocket error")));
ws.onopen = () => facet.turn(() => {
const relay = new Relay.Relay({
debug: true,
debug,
trustPeer: true,
packetWriter: bs => ws.send(bs),
setup(r: Relay.Relay) {