diff --git a/TODO.md b/TODO.md index bb7d9ca..7ddf5ce 100644 --- a/TODO.md +++ b/TODO.md @@ -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(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");` diff --git a/examples/example-simple-chat/package.json b/examples/example-simple-chat/package.json index 191bba5..302faca 100644 --- a/examples/example-simple-chat/package.json +++ b/examples/example-simple-chat/package.json @@ -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", diff --git a/examples/example-simple-chat/protocols/schemas/wsRelay.prs b/examples/example-simple-chat/protocols/schemas/wsRelay.prs index f7a9811..c2e1613 100644 --- a/examples/example-simple-chat/protocols/schemas/wsRelay.prs +++ b/examples/example-simple-chat/protocols/schemas/wsRelay.prs @@ -1,5 +1,7 @@ version 1 . +Resolved = . + ViaRelay = . ForceRelayDisconnect = . diff --git a/examples/example-simple-chat/src/index.ts b/examples/example-simple-chat/src/index.ts index 6faa307..460bc69 100644 --- a/examples/example-simple-chat/src/index.ts +++ b/examples/example-simple-chat/src/index.ts @@ -1,14 +1,96 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones -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(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`
  • ${who}
  • `); + } + + 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(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) { - 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`
  • ${who}
  • `); - } - - 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`${(new Date()).toUTCString()}`; const item = html`
    ${stamp}${item0}
    `; diff --git a/examples/example-simple-chat/src/wsRelay.ts b/examples/example-simple-chat/src/wsRelay.ts index 208b1f2..1e3be6c 100644 --- a/examples/example-simple-chat/src/wsRelay.ts +++ b/examples/example-simple-chat/src/wsRelay.ts @@ -1,22 +1,48 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones -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) {