From 6fb100ee60d8d34dce557928106fbf5c534d8b58 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Fri, 7 Jun 2024 11:53:22 +0200 Subject: [PATCH] Simplify gatekeeper client-side protocols --- examples/example-simple-chat/src/index.ts | 73 +++--- examples/example-simple-chat/yarn.lock | 29 ++- packages/core/protocols/schema-bundle.bin | 4 +- .../core/protocols/schemas/gatekeeper.prs | 23 +- packages/core/protocols/schemas/rpc.prs | 11 + packages/core/src/index.ts | 2 + packages/core/src/runtime/rpc.ts | 51 +++++ packages/core/src/schemas.ts | 1 + packages/create/template/src/index.ts | 11 +- packages/ws-relay/src/index.ts | 210 +++++++----------- 10 files changed, 222 insertions(+), 193 deletions(-) create mode 100644 packages/core/protocols/schemas/rpc.prs create mode 100644 packages/core/src/runtime/rpc.ts diff --git a/examples/example-simple-chat/src/index.ts b/examples/example-simple-chat/src/index.ts index 314803a..b5f5b79 100644 --- a/examples/example-simple-chat/src/index.ts +++ b/examples/example-simple-chat/src/index.ts @@ -1,10 +1,10 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright Ā© 2016-2021 Tony Garnock-Jones -import { fromJS, Bytes, Dataspace, Ref, Sturdy, AnyValue, Reader, Schemas, stringify } from "@syndicate-lang/core"; +import { fromJS, Bytes, Dataspace, Ref, AnyValue, Reader, Schemas, 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 { boot as bootWsRelay, contactRemote } from "@syndicate-lang/ws-relay"; import { Present, Says } from './gen/simpleChatProtocol'; import G = Schemas.gatekeeper; import N = Schemas.noise; @@ -38,47 +38,40 @@ function bootChat(ds: Ref) { console.error(e); } assert UIAttribute('#route', 'class', 'invalid') when (!route); - if (route) contactRemote(route); - } - - function contactRemote(route: G.Route) { - console.log('contactRemote', route); - during G.ResolvePath({ - "route": route, - "addr": $addr, - "control": $control: Ref, - "resolved": G.Resolved.accepted($remoteDs: Ref), - }) => { - 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 }); + if (route) { + console.log('contactRemote', route); + contactRemote(route, (remoteDs, control, addr) => { + 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`
  • ${who}
  • `); + + at remoteDs { + assert 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); + } } - - on message Says({ "who": $who: string, "what": $what: string }) => { - outputUtterance(who, what); - } - } + }, ds); } } } diff --git a/examples/example-simple-chat/yarn.lock b/examples/example-simple-chat/yarn.lock index 7a56907..9b7c568 100644 --- a/examples/example-simple-chat/yarn.lock +++ b/examples/example-simple-chat/yarn.lock @@ -2,30 +2,35 @@ # yarn lockfile v1 -"@preserves/core@0.995.200", "@preserves/core@^0.995.200": +"@preserves/core@^0.995.200": version "0.995.200" resolved "https://registry.yarnpkg.com/@preserves/core/-/core-0.995.200.tgz#65575cf8f9320e73b5d37fa9ac9d6881a33fd3a6" integrity sha512-htZ2x+hltUpKoPsviWSuelzZW96po9zVSsN0RZEih60FahNX0R2LCqFJ6v5lyATuQ9oHALVi0w8w4rtf8oiAJw== -"@preserves/schema-cli@0.995.201": - version "0.995.201" - resolved "https://registry.yarnpkg.com/@preserves/schema-cli/-/schema-cli-0.995.201.tgz#e87cb7ba51b225ff6ea96f177c5388907adf6717" - integrity sha512-n/lpVgWBY1UTRySPXIwWG4Yu87OrQ9b7Xo8zPPQzD/DJDTEDT3y3SUujY7UTF1pQn09xRGnfPnDu1RoRCv2J3w== +"@preserves/core@^0.995.206": + version "0.995.206" + resolved "https://registry.yarnpkg.com/@preserves/core/-/core-0.995.206.tgz#f13507aa39dbc8cbd5d10ac18d107c06e92bf519" + integrity sha512-Uc4SSSXHQG6xYER9cCc0bkbLYM8fGVbO4kXw3DI8BaQjeUFPOX3yYNYPAvRNzmaF4MuMOZY+jSJpjJ9o5g6A0w== + +"@preserves/schema-cli@>=0.995.201": + version "0.995.206" + resolved "https://registry.yarnpkg.com/@preserves/schema-cli/-/schema-cli-0.995.206.tgz#709c96d938dce3bf5b132a3da1304ca3c84bbe2e" + integrity sha512-870rwjKtQHA8huwt8MpMImnKC2cBHKYzcJi4811k5RuYs/HdbT9B81NMSiSn+Hq19/v9fEBicwwEmao3oM7hJw== dependencies: - "@preserves/core" "^0.995.200" - "@preserves/schema" "^0.995.201" + "@preserves/core" "^0.995.206" + "@preserves/schema" "^0.995.206" chalk "^4.1" chokidar "^3.5" commander "^7.2" glob "^7.1" minimatch "^3.0" -"@preserves/schema@^0.995.201": - version "0.995.201" - resolved "https://registry.yarnpkg.com/@preserves/schema/-/schema-0.995.201.tgz#827ad539afb0fdf68ba885ee3ff43db0fd473aa5" - integrity sha512-0SuhwOEAfxtrAeOTZ8V1yNEcHHHEbfUmoFawrF+vzTQFNZRz/X3LF1FM6wOiJlHtb7tBr4B+rHXX8jfNrqrdkA== +"@preserves/schema@^0.995.206": + version "0.995.206" + resolved "https://registry.yarnpkg.com/@preserves/schema/-/schema-0.995.206.tgz#8e35a7db5363374f6794009d394510871f86746e" + integrity sha512-JmXGxSqsNSEzZNpvOYPupSZhffvMIvgnUmKE+Yt5oiPiD/8H52EbKRrqBUQuVpvI4hvwgThWAtNIg3+q42vI2w== dependencies: - "@preserves/core" "^0.995.200" + "@preserves/core" "^0.995.206" "@rollup/pluginutils@^3.0.9": version "3.1.0" diff --git a/packages/core/protocols/schema-bundle.bin b/packages/core/protocols/schema-bundle.bin index b207960..7d2b721 100644 --- a/packages/core/protocols/schema-bundle.bin +++ b/packages/core/protocols/schema-bundle.bin @@ -1,4 +1,4 @@ -“³bundle·µ³tcp„“³schema·³version°³ definitions·³TcpLocal“³rec“³lit³ tcp-local„“³tupleµ“³named³host“³atom³String„„“³named³port“³atom³ SignedInteger„„„„„³ TcpRemote“³rec“³lit³ +“³bundle·µ³rpc„“³schema·³version°³ definitions·³Answer“³rec“³lit³a„“³tupleµ“³named³request³any„“³named³response³any„„„„³Result“³orµµ±ok“³rec“³lit³ok„“³tupleµ“³named³value³any„„„„„µ±error“³rec“³lit³error„“³tupleµ“³named³error³any„„„„„„„³Question“³rec“³lit³q„“³tupleµ“³named³request³any„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³tcp„“³schema·³version°³ definitions·³TcpLocal“³rec“³lit³ tcp-local„“³tupleµ“³named³host“³atom³String„„“³named³port“³atom³ SignedInteger„„„„„³ TcpRemote“³rec“³lit³ tcp-remote„“³tupleµ“³named³host“³atom³String„„“³named³port“³atom³ SignedInteger„„„„„³ TcpPeerInfo“³rec“³lit³tcp-peer„“³tupleµ“³named³handle“³embedded³any„„“³named³local“³refµ„³TcpLocal„„“³named³remote“³refµ„³ TcpRemote„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³http„“³schema·³version°³ definitions·³Chunk“³orµµ±string“³atom³String„„µ±bytes“³atom³ ByteString„„„„³Headers“³dictof“³atom³Symbol„“³atom³String„„³MimeType“³atom³Symbol„³ QueryValue“³orµµ±string“³atom³String„„µ±file“³rec“³lit³file„“³tupleµ“³named³filename“³atom³String„„“³named³headers“³refµ„³Headers„„“³named³body“³atom³ @@ -40,5 +40,5 @@ ByteString RunService“³rec“³lit³ run-service„“³tupleµ“³named³ serviceName³any„„„„³ ServiceState“³rec“³lit³ service-state„“³tupleµ“³named³ serviceName³any„“³named³state“³refµ„³State„„„„„³ ServiceObject“³rec“³lit³service-object„“³tupleµ“³named³ serviceName³any„“³named³object³any„„„„³RequireService“³rec“³lit³require-service„“³tupleµ“³named³ serviceName³any„„„„³RestartService“³rec“³lit³restart-service„“³tupleµ“³named³ serviceName³any„„„„³ServiceDependency“³rec“³lit³ depends-on„“³tupleµ“³named³depender³any„“³named³dependee“³refµ„³ ServiceState„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³protocol„“³schema·³version°³ definitions·³Nop“³lit€„³Oid“³atom³ SignedInteger„³Sync“³rec“³lit³S„“³tupleµ“³named³peer“³embedded“³lit„„„„„„³Turn“³seqof“³refµ„³ TurnEvent„„³Error“³rec“³lit³error„“³tupleµ“³named³message“³atom³String„„“³named³detail³any„„„„³Event“³orµµ±Assert“³refµ„³Assert„„µ±Retract“³refµ„³Retract„„µ±Message“³refµ„³Message„„µ±Sync“³refµ„³Sync„„„„³Assert“³rec“³lit³A„“³tupleµ“³named³ assertion“³refµ„³ Assertion„„“³named³handle“³refµ„³Handle„„„„„³Handle“³atom³ SignedInteger„³Packet“³orµµ±Turn“³refµ„³Turn„„µ±Error“³refµ„³Error„„µ± Extension“³refµ„³ Extension„„µ±Nop“³refµ„³Nop„„„„³Message“³rec“³lit³M„“³tupleµ“³named³body“³refµ„³ Assertion„„„„„³Retract“³rec“³lit³R„“³tupleµ“³named³handle“³refµ„³Handle„„„„„³ Assertion³any³ Extension“³rec“³named³label³any„“³named³fields“³seqof³any„„„³ TurnEvent“³tupleµ“³named³oid“³refµ„³Oid„„“³named³event“³refµ„³Event„„„„„³ embeddedType€„„µ³ dataspace„“³schema·³version°³ definitions·³Observe“³rec“³lit³Observe„“³tupleµ“³named³pattern“³refµ³dataspacePatterns„³Pattern„„“³named³observer“³embedded³any„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³ gatekeeper„“³schema·³version°³ definitions·³Bind“³rec“³lit³bind„“³tupleµ“³named³ description“³refµ„³ Description„„“³named³target“³embedded³any„„“³named³observer“³refµ„³ BindObserver„„„„„³Step“³rec“³named³stepType“³atom³Symbol„„“³tupleµ“³named³detail³any„„„„³Bound“³orµµ±bound“³rec“³lit³bound„“³tupleµ“³named³pathStep“³refµ„³PathStep„„„„„„µ±Rejected“³refµ„³Rejected„„„„³Route“³rec“³lit³route„“³ tuplePrefixµ“³named³ -transports“³seqof³any„„„“³named³ pathSteps“³seqof“³refµ„³PathStep„„„„„³Resolve“³rec“³lit³resolve„“³tupleµ“³named³step“³refµ„³Step„„“³named³observer“³embedded“³refµ„³Resolved„„„„„„³PathStep“³rec“³named³stepType“³atom³Symbol„„“³tupleµ“³named³detail³any„„„„³Rejected“³rec“³lit³rejected„“³tupleµ“³named³detail³any„„„„³Resolved“³orµµ±accepted“³rec“³lit³accepted„“³tupleµ“³named³responderSession“³embedded³any„„„„„„µ±Rejected“³refµ„³Rejected„„„„³ Description“³rec“³named³stepType“³atom³Symbol„„“³tupleµ“³named³detail³any„„„„³ ResolvePath“³rec“³lit³ resolve-path„“³tupleµ“³named³route“³refµ„³Route„„“³named³addr³any„“³named³control“³embedded“³refµ„³TransportControl„„„“³named³resolved“³refµ„³Resolved„„„„„³ BindObserver“³orµµ±present“³embedded“³refµ„³Bound„„„µ±absent“³lit€„„„„³ForceDisconnect“³rec“³lit³force-disconnect„“³tupleµ„„„³ResolvedPathStep“³rec“³lit³ path-step„“³tupleµ“³named³origin“³embedded“³refµ„³Resolve„„„“³named³pathStep“³refµ„³PathStep„„“³named³resolved“³refµ„³Resolved„„„„„³TransportControl“³refµ„³ForceDisconnect„³TransportConnection“³rec“³lit³connect-transport„“³tupleµ“³named³addr³any„“³named³control“³embedded“³refµ„³TransportControl„„„“³named³resolved“³refµ„³Resolved„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³transportAddress„“³schema·³version°³ definitions·³Tcp“³rec“³lit³tcp„“³tupleµ“³named³host“³atom³String„„“³named³port“³atom³ SignedInteger„„„„„³Unix“³rec“³lit³unix„“³tupleµ“³named³path“³atom³String„„„„„³Stdio“³rec“³lit³stdio„“³tupleµ„„„³ WebSocket“³rec“³lit³ws„“³tupleµ“³named³url“³atom³String„„„„„„³ embeddedType€„„µ³dataspacePatterns„“³schema·³version°³ definitions·³AnyAtom“³orµµ±bool“³atom³Boolean„„µ±double“³atom³Double„„µ±int“³atom³ SignedInteger„„µ±string“³atom³String„„µ±bytes“³atom³ +transports“³seqof³any„„„“³named³ pathSteps“³seqof“³refµ„³PathStep„„„„„³Resolve“³rec“³lit³resolve„“³tupleµ“³named³step“³refµ„³Step„„“³named³observer“³embedded“³refµ„³Resolved„„„„„„³PathStep“³rec“³named³stepType“³atom³Symbol„„“³tupleµ“³named³detail³any„„„„³Rejected“³rec“³lit³rejected„“³tupleµ“³named³detail³any„„„„³Resolved“³orµµ±accepted“³rec“³lit³accepted„“³tupleµ“³named³responderSession“³embedded³any„„„„„„µ±Rejected“³refµ„³Rejected„„„„³ Description“³rec“³named³stepType“³atom³Symbol„„“³tupleµ“³named³detail³any„„„„³ ResolvePath“³rec“³lit³ resolve-path„“³tupleµ“³named³route“³refµ„³Route„„„„„³ BindObserver“³orµµ±present“³embedded“³refµ„³Bound„„„µ±absent“³lit€„„„„³ ResolvedPath“³rec“³lit³ resolved-path„“³tupleµ“³named³addr³any„“³named³control“³embedded“³refµ„³TransportControl„„„“³named³responderSession“³embedded³any„„„„„³ForceDisconnect“³rec“³lit³force-disconnect„“³tupleµ„„„³ResolvePathStep“³rec“³lit³resolve-path-step„“³tupleµ“³named³origin“³embedded“³refµ„³Resolve„„„“³named³pathStep“³refµ„³PathStep„„„„„³ConnectTransport“³rec“³lit³connect-transport„“³tupleµ“³named³addr³any„„„„³ResolvedPathStep“³embedded³any„³TransportControl“³refµ„³ForceDisconnect„³ConnectedTransport“³rec“³lit³connected-transport„“³tupleµ“³named³addr³any„“³named³control“³embedded“³refµ„³TransportControl„„„“³named³responderSession“³embedded³any„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„µ³transportAddress„“³schema·³version°³ definitions·³Tcp“³rec“³lit³tcp„“³tupleµ“³named³host“³atom³String„„“³named³port“³atom³ SignedInteger„„„„„³Unix“³rec“³lit³unix„“³tupleµ“³named³path“³atom³String„„„„„³Stdio“³rec“³lit³stdio„“³tupleµ„„„³ WebSocket“³rec“³lit³ws„“³tupleµ“³named³url“³atom³String„„„„„„³ embeddedType€„„µ³dataspacePatterns„“³schema·³version°³ definitions·³AnyAtom“³orµµ±bool“³atom³Boolean„„µ±double“³atom³Double„„µ±int“³atom³ SignedInteger„„µ±string“³atom³String„„µ±bytes“³atom³ ByteString„„µ±symbol“³atom³Symbol„„µ±embedded“³embedded³any„„„„³Pattern“³orµµ±discard“³rec“³lit³_„“³tupleµ„„„„µ±bind“³rec“³lit³bind„“³tupleµ“³named³pattern“³refµ„³Pattern„„„„„„µ±lit“³rec“³lit³lit„“³tupleµ“³named³value“³refµ„³AnyAtom„„„„„„µ±group“³rec“³lit³group„“³tupleµ“³named³type“³refµ„³ GroupType„„“³named³entries“³dictof³any“³refµ„³Pattern„„„„„„„„„³ GroupType“³orµµ±rec“³rec“³lit³rec„“³tupleµ“³named³label³any„„„„„µ±arr“³rec“³lit³arr„“³tupleµ„„„„µ±dict“³rec“³lit³dict„“³tupleµ„„„„„„„³ embeddedType“³refµ³ EntityRef„³Cap„„„„„ \ No newline at end of file diff --git a/packages/core/protocols/schemas/gatekeeper.prs b/packages/core/protocols/schemas/gatekeeper.prs index 9ba662c..ba64c3f 100644 --- a/packages/core/protocols/schemas/gatekeeper.prs +++ b/packages/core/protocols/schemas/gatekeeper.prs @@ -52,14 +52,23 @@ Bound = / Rejected . # --------------------------------------------------------------------------- # Protocol at client-side dataspaces, for resolution utilities -# Assertion. In response to observation of this with appropriate captures/wildcards in `addr` -# and `resolved`, respondent will follow `route.pathSteps` starting from one of the -# `route.transports`, asserting `ResolvePath` with the final `Resolved` as well as the selected -# transport `addr` and a `control` for it. -ResolvePath = . +# The client-side operates using `rpc.Question`s and `rpc.Answer`s. -TransportConnection = . -ResolvedPathStep = . +# Assert `rpc.Question` with `ResolvePath` to request resolution of `Route`. The resolution +# utility will continuously try to satisfy the request, following `route.pathSteps` starting +# from one of the `route.transports`, ultimately asserting `rpc.Answer` with an `rpc.Result` in +# response. If the process completes successfully, the `rpc.Result.ok` will carry a +# `ResolvedPath`. +ResolvePath = . +ResolvedPath = . + +# Assertions. As `ResolvePath`/`ResolvedPath`, but just for an initial transport link setup. +ConnectTransport = . +ConnectedTransport = . + +# Assertions. Like `ResolvePath`/`ResolvedPath`, but for incremental resolution along a route. +ResolvePathStep = . +ResolvedPathStep = #:any . PathStep = < @stepType symbol [@detail any]> . # A `Route` describes a network path that can be followed to reach some target entity. diff --git a/packages/core/protocols/schemas/rpc.prs b/packages/core/protocols/schemas/rpc.prs new file mode 100644 index 0000000..ec839c9 --- /dev/null +++ b/packages/core/protocols/schemas/rpc.prs @@ -0,0 +1,11 @@ +version 1 . +embeddedType EntityRef.Cap . + +# Assertion. Establishes a frame for a request. +Question = . + +# Assertion or message. Responds to a question. +Answer = . + +# Value. Captures a common "ok-or-error" pattern as seen in e.g. Rust. +Result = / . diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0cc563e..2de05e5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,8 @@ export * as Pattern from './runtime/pattern.js'; export * as QuasiValue from './runtime/quasivalue.js'; export * from './runtime/randomid.js'; export * as Rewrite from './runtime/rewrite.js'; +export * from './runtime/rpc.js'; +export * as Rpc from './runtime/rpc.js'; export * as Skeleton from './runtime/skeleton.js'; export * from './runtime/space.js'; export * from './runtime/supervise.js'; diff --git a/packages/core/src/runtime/rpc.ts b/packages/core/src/runtime/rpc.ts new file mode 100644 index 0000000..31d50a6 --- /dev/null +++ b/packages/core/src/runtime/rpc.ts @@ -0,0 +1,51 @@ +/// SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-FileCopyrightText: Copyright Ā© 2024 Tony Garnock-Jones + +import { Assertable, Ref } from './actor.js'; +import { ctor, dict, QuasiValue } from './quasivalue.js'; +import * as R from '../gen/rpc.js'; +import { fromJS } from '@preserves/core'; + +export { Result } from '../gen/rpc.js'; + +export function Question(q: Q): R.Question { + return R.Question(fromJS(q)); +} + +export namespace Question { + export function quasiValue(q: QuasiValue): QuasiValue { + return ctor(R.Question, q); + } +} + +export function Answer(q: Q, a: A): R.Answer { + return R.Answer({ request: fromJS(q), response: fromJS(a) }); +} + +export namespace Answer { + export function quasiValue(q: QuasiValue, a: QuasiValue): QuasiValue { + return ctor(R.Answer, dict( + ["request", q], + ["response", a])); + } +} + +export function resultOk(v: V): R.Result { + return R.Result.ok(fromJS(v)); +} + +export namespace resultOk { + export function quasiValue(v: QuasiValue): QuasiValue { + return ctor(R.Result.ok, v); + } +} + +export function resultError(e: E): R.Result { + return R.Result.error(fromJS(e)); +} + +export namespace resultError { + export function quasiValue(e: QuasiValue): QuasiValue { + return ctor(R.Result.error, e); + } +} diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index 8f09d35..c260299 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -6,6 +6,7 @@ export * as dataspace from './gen/dataspace.js'; export * as gatekeeper from './gen/gatekeeper.js'; export * as protocol from './gen/protocol.js'; export * as noise from './gen/noise.js'; +export * as rpc from './gen/rpc.js'; export * as service from './gen/service.js'; export * as stdenv from './gen/stdenv.js'; export * as stream from './gen/stream.js'; diff --git a/packages/create/template/src/index.ts b/packages/create/template/src/index.ts index 853fbf5..6b1cc7f 100644 --- a/packages/create/template/src/index.ts +++ b/packages/create/template/src/index.ts @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright Ā© 2023-2024 Tony Garnock-Jones -import { Dataspace, Ref, Sturdy, Reader, Schemas, Embedded, randomId, fromJS } from "@syndicate-lang/core"; +import { Dataspace, Ref, Sturdy, Reader, Schemas, Embedded, randomId, fromJS, Question, resultOk, Answer } from "@syndicate-lang/core"; import { boot as bootHtml, Widget } from "@syndicate-lang/html2"; import wsRelay from "@syndicate-lang/ws-relay"; import { ExampleDefinition } from './gen/example'; @@ -34,11 +34,10 @@ function bootApp(ds: Ref) { '').next())))], }); - during G.ResolvePath({ - "route": route, - "resolved": G.Resolved.accepted($remoteDs_e: Embedded), - }) => { - const remoteDs = remoteDs_e.embeddedValue; + assert Question(G.ResolvePath(route)); + during Answer(G.ResolvePath(route), resultOk(G.ResolvedPath({ + responderSession: $remoteDs: Ref, + }))) => { at remoteDs { assert ExampleDefinition(this_instance); during ExampleDefinition($who: string) => { diff --git a/packages/ws-relay/src/index.ts b/packages/ws-relay/src/index.ts index bd41123..6608520 100644 --- a/packages/ws-relay/src/index.ts +++ b/packages/ws-relay/src/index.ts @@ -7,7 +7,6 @@ import { Bytes, Dataspace, IdentitySet, - Observe, QuasiValue as Q, Record, Ref, @@ -19,23 +18,28 @@ import { canonicalEncode, decode, fromJS, - isEmbedded, parse, stringify, underlying, Embeddable, SaltyCrypto, + Question, + Answer, + Result, + resultOk, + resultError, } from "@syndicate-lang/core"; import G = Schemas.gatekeeper; import S = Schemas.sturdy; import N = Schemas.noise; import T = Schemas.transportAddress; import E = Schemas.stdenv; +import R = Schemas.rpc; export * from './seal.js'; type TransportState = { - addr: T.WebSocket, + addr: AnyValue, control: Ref, peer: Ref, }; @@ -43,23 +47,17 @@ type TransportState = { export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketConstructor?: typeof WebSocket) { spawn named 'transportConnector' { at ds { - during Observe({ "pattern": :pattern G.TransportConnection({ - "addr": \$addrPatValue, - "control": \_, - "resolved": \_, - }) }) => { - const addr = Q.drop_lit(addrPatValue, T.toWebSocket); - if (!addr) return; + during Question(G.ConnectTransport(T.WebSocket($url: string))) => { let counter =0; - Supervisor.always(() => ['transportConnector', fromJS(addr), counter++], () => { - console.log('connecting', addr.url, counter); - connectTo(addr); + Supervisor.always(() => ['wsTransport', fromJS(url), counter++], () => { + console.log('connecting', url, counter); + connectWs(url); }); } } } - function connectTo(addr: T.WebSocket) { + function connectWs(url: string) { const facet = Turn.activeFacet; facet.preventInertCheck(); const controlEntity = { @@ -73,9 +71,9 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons function succeed(ws: WebSocket) { if (final) return; final = true; - console.log('opened', addr.url); + console.log('opened', url); on stop { - console.log('closing', addr.url); + console.log('closing', url); ws.close(); } ws.onclose = () => facet.turn(() => { stop facet; }); @@ -87,60 +85,50 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons setup: r => ws.onmessage = e => r.accept(new Uint8Array(e.data)), initialOid: 0, }); - console.log('succeed', addr.url); + console.log('succeed', url); at ds { - assert G.TransportConnection({ - "addr": fromJS(addr), + assert Answer(G.ConnectTransport(fromJS(T.WebSocket(url))), resultOk(G.ConnectedTransport({ + "addr": fromJS(T.WebSocket(url)), "control": create controlEntity, - "resolved": G.Resolved.accepted(relay.peer!), - }); + "responderSession": relay.peer!, + }))); } } function fail(detail: Assertion) { if (final) return; final = true; - console.log('fail', addr.url, detail); + console.log('fail', url, detail); at ds { - assert G.TransportConnection({ - "addr": fromJS(addr), - "control": create controlEntity, - "resolved": G.Resolved.Rejected(G.Rejected(detail)), - }); + assert Answer(G.ConnectTransport(fromJS(T.WebSocket(url))), resultError(detail)); } setTimeout(() => facet.turn(() => { stop facet; }), 10000); } try { - const ws = new (WebSocketConstructor ?? WebSocket)(addr.url); + const ws = new (WebSocketConstructor ?? WebSocket)(url); ws.binaryType = 'arraybuffer'; ws.onopen = () => facet.turn(() => succeed(ws)); ws.onclose = () => facet.turn(() => fail(Symbol.for('closed'))); ws.onerror = () => facet.turn(() => fail(Symbol.for('websocket-error-event'))); } catch (e) { - console.error('Failed opening websocket', addr.url, e); + console.error('Failed opening websocket', url, e); fail(Symbol.for('websocket-exception')); } } spawn named 'pathResolver' { at ds { - during Observe({ "pattern": :pattern G.ResolvePath({ - "route": \$routePatValue, - "addr": \_, - "control": \_, - "resolved": \_, - }) }) => { - const route = Q.drop_lit(routePatValue, G.toRoute); + during Question(G.ResolvePath($route0)) => { + const route = G.toRoute(route0); if (!route) return; field candidates: IdentitySet = new IdentitySet(); - route.transports.forEach(t => { - const addr = T.toWebSocket(t); - if (!addr) return; - console.log('tracking', addr.url); - during G.TransportConnection({ + route.transports.forEach(addr => { + console.log('tracking', addr); + assert Question(G.ConnectTransport(addr)); + during Answer(G.ConnectTransport(addr), resultOk(G.ConnectedTransport({ "addr": addr, "control": $control: Ref, - "resolved": G.Resolved.accepted($peer: Ref), - }) => { + "responderSession": $peer: Ref, + }))) => { const entry = { addr, control, peer }; candidates.value.add(entry); candidates.changed(); @@ -161,39 +149,34 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons } rootPeer.value = best.value?.peer ?? null; } - resolve(() => rootPeer.value, route.pathSteps, (r) => { - console.log('leaf', best.value?.addr?.url); - assert G.ResolvePath({ - "route": route, - "addr": fromJS(best.value!.addr), + resolve(() => ({ ref: rootPeer.value, error: false }), route.pathSteps, (r) => { + console.log('leaf', best.value?.addr); + assert Answer(G.ResolvePath(route), resultOk(G.ResolvedPath({ + "addr": best.value!.addr, "control": best.value!.control, - "resolved": r()! - }) when (r()); + "responderSession": r().ref!, + }))) when (r().ref); + assert Answer(G.ResolvePath(route), resultError(r().error)) when (!r().ref); }); } } } function resolve( - e: () => Ref | null, + e: () => { ref: Ref | null, error: AnyValue }, // gross steps: G.PathStep[], - k: (r: () => G.Resolved | null) => void, + k: (r: () => { ref: Ref | null, error: AnyValue }) => void, ) { if (steps.length === 0) { - k(() => { - const peer = e(); - return peer === null ? null : G.Resolved.accepted(peer); - }); + k(e); } else { const [step, ...more] = steps; at ds { - during G.ResolvedPathStep({ - "origin": (e()!), - "pathStep": step, - "resolved": $resolved: G.Resolved, - }) when (e()) => { - switch (resolved._variant) { - case "accepted": + const q = () => G.ResolvePathStep({ origin: (e().ref!), pathStep: step }); + assert Question(q()) when (e().ref); + during Answer(((q)()) /* omg */, $a: Result) when (e().ref) => { + switch (a._variant) { + case "ok": // Include a call to our e() in the e we pass in to the recursive // call to resolve(). e() returning non-null is a precondition for // the call; if that precondition ever changes, we want to NOT @@ -208,10 +191,18 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons // best.value!, which is now null, boom. With the call to e(), we // short circuit and so the assertion becomes null at that point. // - resolve(() => e() && resolved.responderSession, more, k); + const nextRef = G.toResolvedPathStep(a.value); + const nextResult = (nextRef === void 0) + ? { ref: null, error: 'bad ResolvedPathStep' } + : { ref: nextRef, error: false }; + const nextE = () => { + const result = e(); + return result.ref ? nextResult : result; + }; + resolve(nextE, more, k); break; - case "Rejected": - k(() => resolved); + case "error": + k(() => ({ ref: null, error: a.error })); break; } } @@ -221,22 +212,13 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons spawn named 'noiseStep' { at ds { - during Observe({ "pattern": :pattern G.ResolvedPathStep({ - "origin": \$originPatValue, - "pathStep": G.PathStep({ - "stepType": N.$noise, - "detail": \$detailPatValue, + during Question($q(G.ResolvePathStep({ + origin: $origin: Ref, + pathStep: G.PathStep({ + stepType: N.$noise, + detail: $spec: N.NoisePathStepDetail, }), - "resolved": \_, - }) }) => { - const origin0 = Q.drop_lit(originPatValue); - if (!origin0 || !isEmbedded(origin0)) return; - const origin = origin0; - - const spec0 = Q.drop_lit(detailPatValue, N.toNoisePathStepDetail); - if (!spec0) return; - const spec = spec0; - + }))) => { const algorithms = SaltyCrypto.Noise_25519_ChaChaPoly_BLAKE2s; const protocol = spec.protocol._variant === "present" ? spec.protocol.protocol : @@ -270,14 +252,7 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons case "Rejected": stop { at ds { - assert G.ResolvedPathStep({ - "origin": origin, - "pathStep": G.PathStep({ - "stepType": N.$noise, - "detail": fromJS(N.NoisePathStepDetail(spec)), - }), - "resolved": response, - }); + assert Answer(q, resultError(response.value.detail)); } } } @@ -332,14 +307,7 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons setup: r => relay = r, initialOid: 0, }).peer!; - assert G.ResolvedPathStep({ - "origin": origin, - "pathStep": G.PathStep({ - "stepType": N.$noise, - "detail": fromJS(spec), - }), - "resolved": G.Resolved.accepted(peer), - }); + assert Answer(q, resultOk(peer)); } function handlePacket(body: Assertion) { @@ -368,21 +336,13 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons spawn named 'sturdyRefStep' { at ds { - during Observe({ "pattern": :pattern G.ResolvedPathStep({ - "origin": \$originPatValue, - "pathStep": G.PathStep({ - "stepType": S.$ref, - "detail": \$detailPatValue, + during Question($q(G.ResolvePathStep({ + origin: $origin: Ref, + pathStep: G.PathStep({ + stepType: S.$ref, + detail: $parameters: S.SturdyPathStepDetail, }), - "resolved": \_, - }) }) => { - const origin0 = Q.drop_lit(originPatValue); - if (!origin0 || !isEmbedded(origin0)) return; - const origin = origin0; - - const parameters = Q.drop_lit(detailPatValue, S.toSturdyPathStepDetail); - if (!parameters) return; - + }))) => { at origin { assert G.Resolve({ "step": G.Step({ @@ -392,15 +352,13 @@ export function boot(ds = Dataspace.local, debug: boolean = false, WebSocketCons "observer": create assertionFacetObserver(e => { const response = G.toResolved(e); if (!response) return; - at ds { - assert G.ResolvedPathStep({ - "origin": origin, - "pathStep": G.PathStep({ - "stepType": S.$ref, - "detail": fromJS(parameters), - }), - "resolved": response, - }); + switch (response._variant) { + case "accepted": + assert Answer(q, resultOk(response.responderSession)); + break; + case "Rejected": + assert Answer(q, resultError(response.value.detail)); + break; } }), }); @@ -465,7 +423,7 @@ export function decodeStandardRoute(s: string): G.Route | null { } export function contactRemote( - route: G.Route | Record, Ref>, + route: G.Route | Record, Ref> , connectedFacet: ( remoteObject: Ref, controlObject: Ref, @@ -473,14 +431,14 @@ export function contactRemote( ) => void, ds = Dataspace.local, ) { - const routeValue = 'pathSteps' in route ? G.fromRoute(G.Route(route)) : route; + const routeValue = 'pathSteps' in route ? route : G.asRoute(route); at ds { - during G.ResolvePath({ - "route": routeValue, + assert Question(G.ResolvePath(routeValue)); + during Answer(G.ResolvePath(routeValue), resultOk(G.ResolvedPath({ "addr": $addr, "control": $control: Ref, - "resolved": G.Resolved.accepted($resolved: Ref), - }) => { + "responderSession": $resolved: Ref, + }))) => { connectedFacet(resolved, control, addr); } }