From 823503e991f681c5ca7027a160c6015d837e6f7d Mon Sep 17 00:00:00 2001 From: Cameron James Date: Thu, 8 Feb 2024 13:03:44 -0800 Subject: [PATCH 01/17] Fix typos --- _config.yml | 2 +- src/_backend/warp.md | 8 ++++---- src/_meta/index.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/_config.yml b/_config.yml index cf1d580..a242100 100644 --- a/_config.yml +++ b/_config.yml @@ -81,7 +81,7 @@ collections: kind: docs output: true name: "Backend" - description: Documentation about SwimOS fundamentals and CLI useage. + description: Documentation about SwimOS fundamentals and CLI usage. permalink: /backend/:name/ order: - index.md diff --git a/src/_backend/warp.md b/src/_backend/warp.md index 15df379..7b32cd3 100644 --- a/src/_backend/warp.md +++ b/src/_backend/warp.md @@ -9,14 +9,14 @@ redirect_from: - /reference/warp.html --- -The internet routes packets between physical or virtual machines, identified by IP addresses. On top of the internet sites the World Wide Web you're using right now, which routes requests and responses for Web Resources, identified by URIs. The Web was built for documents. But treating everything like a document is quite limiting. A smart city isn't a document; it's a continuously evolving digital mirror of reality. +The internet routes packets between physical or virtual machines, identified by IP addresses. On top of the internet sits the World Wide Web you're using right now, which routes requests and responses for Web Resources, identified by URIs. The Web was built for documents. But treating everything like a document is quite limiting. A smart city isn't a document; it's a continuously evolving digital mirror of reality. -Things that continuously evolve—like smart cities, autonomous control systems, and collaborative applications—are best modeled as streams of state changes. We can't—and don't want to—give every logical thing in the world an IP address; we want to give things logical names, i.e. URIs. It would be nice if we had a way to route packets between URIs—like an application layer network. +Things that continuously evolve — like smart cities, autonomous control systems, and collaborative applications — are best modeled as streams of state changes. We can't — and don't want to — give every logical thing in the world an IP address; we want to give things logical names, i.e. URIs. It would be nice if we had a way to route packets between URIs — like an application layer network. In recent years, the WebSocket protocol has emerged, enabling us to open streaming connections to URIs. This is great. But it only solves part of the problem. Applications need to open a new WebSocket connection for every URI to which they want to connect. This limitation prevents WebSockets alone from serving as a streaming layer of the World Wide Web. But that's OK; that's not WebSocket's role. -WARP is a protocol for multiplexing bi-directional streams between large numbers of URIs over a single WebSocket connection. Multiplexed streams within a WARP connection are called **links**. Just as the World Wide Web has hypertex links between Web Pages, WARP enables actively streaming links between Web Agents. +WARP is a protocol for multiplexing bi-directional streams between large numbers of URIs over a single WebSocket connection. Multiplexed streams within a WARP connection are called **links**. Just as the World Wide Web has hypertext links between Web Pages, WARP enables actively streaming links between Web Agents. WARP connections exchange WebSocket frames encoded as Recon structures. The use of Recon enables efficient parsing and routing, while preserving the flexibility and extensibility of protocols like HTTP. And the expressiveness of Recon avoids the explosion of specialized grammars and parsers that has caused the originally simple and elegant HTTP protocol to become bloated and complex. It's also worth noting that, although HTTP/2 introduces a limited form of multiplexing, it multiplexes RPC calls, not full-duplex streams. -Having a multiplexed streaming protocol requires a stateful application server to efficiently implement it. Stateful application servers differ from traditional, stateless app servers, in almost every detail—for the better. Swim is a self-contained, stateful application server that implements the WARP multiplexed streaming protocol. +Having a multiplexed streaming protocol requires a stateful application server to efficiently implement it. Stateful application servers differ from traditional, stateless app servers, in almost every detail — for the better. Swim is a self-contained, stateful application server that implements the WARP multiplexed streaming protocol. diff --git a/src/_meta/index.md b/src/_meta/index.md index 2ac78e6..9b21f1c 100644 --- a/src/_meta/index.md +++ b/src/_meta/index.md @@ -9,6 +9,6 @@ This section contains information on managing and contributing to this docs port - **Installation:** Please see installation instructions in the [Contributing Guide]({% link _meta/contribute.md %}) - **[Diátaxis Framework]({% link _meta/diataxis.md %}):** Information on how documents are organized and which types of content belong in which sections. -- **[Styles]({% link _meta/styles.md %}):** Configure Tailwind CSS and see previews/useage information for callouts (info, warning, note, etc) and other CSS styles. +- **[Styles]({% link _meta/styles.md %}):** Configure Tailwind CSS and see previews/usage information for callouts (info, warning, note, etc) and other CSS styles. {% include docs-listing.html %} From 1f49ad15241b24cceccd349df14cb30bfbaedb68 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Tue, 27 Feb 2024 00:08:07 -0800 Subject: [PATCH 02/17] Add foundation for frontend, WARP Client documentation --- _config.yml | 30 ++++ _data/header-nav.yml | 6 + _includes/docs-header.html | 2 +- assets/images/data-model-item-family.svg | 31 ++++ package.json | 3 +- src/_backend/downlinks.md | 14 +- src/_backend/fundamentals.md | 2 +- src/_backend/links.md | 2 +- src/_backend/swim-libraries.md | 15 +- src/_backend/warp.md | 4 +- src/_frontend/dataModel.md | 29 ++++ src/_frontend/downlinks.md | 110 +++++++++++++ src/_frontend/eventDownlink.md | 38 +++++ src/_frontend/form.md | 65 ++++++++ src/_frontend/gettingStarted.md | 119 ++++++++++++++ src/_frontend/index.md | 9 ++ src/_frontend/listDownlink.md | 90 +++++++++++ src/_frontend/mapDownlink.md | 83 ++++++++++ src/_frontend/structures.md | 189 ++++++++++++++++++++++ src/_frontend/valueDownlink.md | 82 ++++++++++ src/_frontend/warpClient.md | 192 +++++++++++++++++++++++ src/_frontend/whatIsWarp.md | 24 +++ src/_tutorials/transit.md | 4 +- 23 files changed, 1115 insertions(+), 28 deletions(-) create mode 100644 assets/images/data-model-item-family.svg create mode 100644 src/_frontend/dataModel.md create mode 100644 src/_frontend/downlinks.md create mode 100644 src/_frontend/eventDownlink.md create mode 100644 src/_frontend/form.md create mode 100644 src/_frontend/gettingStarted.md create mode 100644 src/_frontend/index.md create mode 100644 src/_frontend/listDownlink.md create mode 100644 src/_frontend/mapDownlink.md create mode 100644 src/_frontend/structures.md create mode 100644 src/_frontend/valueDownlink.md create mode 100644 src/_frontend/warpClient.md create mode 100644 src/_frontend/whatIsWarp.md diff --git a/_config.yml b/_config.yml index a242100..9c04671 100644 --- a/_config.yml +++ b/_config.yml @@ -128,6 +128,36 @@ collections: - recon.md - warp.md weight: 1 + frontend: + kind: docs + output: true + name: "Frontend" + description: Documentation about streaming data from SwimOS apps for use in UIs. + permalink: /frontend/:name/ + order: + - index.md + # Introduction + - whatIsWarp.md + - gettingStarted.md + # Connections + - warpClient.md + - downlinks.md + - valueDownlink.md + - mapDownlink.md + - listDownlink.md + - eventDownlink.md + # - property.md + # - demandLanes.md + # - authentication.md + # Data + - dataModel.md + - structures.md + - form.md + # - structures.md + # Examples/Demos + # Integrations + # - react.md + weight: 1 tutorials: kind: docs output: true diff --git a/_data/header-nav.yml b/_data/header-nav.yml index 77e57f5..1fde170 100644 --- a/_data/header-nav.yml +++ b/_data/header-nav.yml @@ -53,6 +53,12 @@ blurb: icon: "fa-solid fa-server" url: /backend/ + - group: Frontend + items: + - title: Frontend Documentation + blurb: + icon: "fa-solid fa-code" + url: /frontend/ - group: Tutorials & Blog items: - title: Tutorials diff --git a/_includes/docs-header.html b/_includes/docs-header.html index 00d003f..a5932d7 100644 --- a/_includes/docs-header.html +++ b/_includes/docs-header.html @@ -103,4 +103,4 @@ - \ No newline at end of file + diff --git a/assets/images/data-model-item-family.svg b/assets/images/data-model-item-family.svg new file mode 100644 index 0000000..8597119 --- /dev/null +++ b/assets/images/data-model-item-family.svg @@ -0,0 +1,31 @@ + + + + + + + + Item + + Field + + + Attr + Slot + + Value + + + + + + + + Record + Data + Text + Num + Bool + Extant + Absent + diff --git a/package.json b/package.json index e016073..1cb0ba6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "## Developing Locally", "main": "postcss.config.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "launch": "bundle exec jekyll serve --livereload" }, "author": "", "license": "ISC", diff --git a/src/_backend/downlinks.md b/src/_backend/downlinks.md index cbf88f2..f7fbcc3 100644 --- a/src/_backend/downlinks.md +++ b/src/_backend/downlinks.md @@ -31,14 +31,8 @@ There are two big things to manage when dealing with downlinks: **data** and **c ### Declaration -#### Java - All downlink classes can be imported from package `swim.api.downlink`. -#### Javascript - -All downlink types are available after including `swim-core.js`, available from https://cdn.swimos.org/js/3.9.0/swim-core.js. - ### Usage Downlinks must be instantiated against Swim refs, i.e. specific server-side or client-side objects. Although several permutations exist, the builder pattern is the same each time: @@ -46,7 +40,7 @@ Downlinks must be instantiated against Swim refs, i.e. specific server-side or c 1. Invoke `downlink()` against your ref for an event downlink, or `downlinkFoo()` for a foo downlink (e.g. `downlinkMap()` for a map downlink) 1. Build the downlink's `hostUri` using `hostUri()` (this step can only be omitted if your Swim ref is server-side, and you are targeting a lane within the same server), the downlink's `nodeUri` using `nodeUri()`, and the downlink's `laneUri` using `laneUri()` 1. Override any lifecycle callback functions, which default to no-ops -1. In strongly-typed languages (Java, Typescript), optionally parametrize the downlink +1. Optionally parametrize the downlink 1. Optionally set the **keepSynced** (pull all existing data from a lane before processing new updates; defaults to `false`) and **keepLinked** (enable consistent **reads** from the downlink (unnecessary for write-only downlinks); defaults to `true`) flags 1. Invoke `open()` on the downlink to initiate data flow 1. When finished, invoke `close()` on the downlink to stop data flow @@ -97,9 +91,11 @@ class CustomClient { Server-side, downlinks are explained in the [Server Downlinks guide]({% link _backend/server-downlinks.md %}). -#### Javascript +### JavaScript + +For details on using downlinks with JavaScript, visit the [**downlinks**]({% link _frontend/downlinks.md %}) article in our [**frontend documentation**](/frontend) -The tutorial application demonstrates [using value downlinks](https://github.com/swimos/tutorial/blob/master/ui/pie.html#L58-L67){:data-proofer-ignore=''} and [map downlinks](https://github.com/swimos/tutorial/blob/master/ui/chart.html#L69-L79){:data-proofer-ignore=''} issued against a Swim client instance. Note the language-level loss of parametrization, but the otherwise-identical syntax to Java. +Furthermore, the tutorial application demonstrates [using value downlinks](https://github.com/swimos/tutorial/blob/master/ui/pie.html#L58-L67){:data-proofer-ignore=''} and [map downlinks](https://github.com/swimos/tutorial/blob/master/ui/chart.html#L69-L79){:data-proofer-ignore=''} issued against a Swim client instance. Note the language-level loss of parametrization, but the otherwise-identical syntax to Java. ### Try It Yourself diff --git a/src/_backend/fundamentals.md b/src/_backend/fundamentals.md index 868295e..f24daed 100644 --- a/src/_backend/fundamentals.md +++ b/src/_backend/fundamentals.md @@ -57,7 +57,7 @@ If Web Agents are distributed objects, then [lanes]({% link _backend/lanes.md %} ### Links -Distributed objects need a way to communicate. [Links]({% link _backend/links.md %}) establishes active references to lanes of Web Agents, transparently streaming bi-directional state changes to keep all parts of an application in sync, without the overhead of queries or remote procedure calls. +Distributed objects need a way to communicate. [Links]({% link _backend/links.md %}) establishes active references to lanes of Web Agents, transparently streaming bidirectional state changes to keep all parts of an application in sync, without the overhead of queries or remote procedure calls. ### Recon diff --git a/src/_backend/links.md b/src/_backend/links.md index 964d63d..c2639fb 100644 --- a/src/_backend/links.md +++ b/src/_backend/links.md @@ -15,7 +15,7 @@ Native machine pointers also trigger transparent message passing between CPU cor Links unify edges in an object graph, pointers in a cache coherency system, and subscriptions to mesaging topics, into a single, ultra high performance, easy to use programming primitive. -Links are bi-directional. Either end of a link can update its implicitly shared state in an eventually consistent manner. A link has a downlink side, and an uplink side. The **downlink** side is the one held by the endpoint that opened the link. The **uplink** side is the one held by the endpoint that received the link request. +Links are bidirectional. Either end of a link can update its implicitly shared state in an eventually consistent manner. A link has a downlink side, and an uplink side. The **downlink** side is the one held by the endpoint that opened the link. The **uplink** side is the one held by the endpoint that received the link request. To open a link, you create a downlink, and specify the address of the Web Agent (called the **node URI**), and the name of the lane (called the **lane URI**) to which you want to link. It's easier than it sounds. Here's what opening a link to a `ValueLane` looks like: diff --git a/src/_backend/swim-libraries.md b/src/_backend/swim-libraries.md index 9d12916..8a0b63d 100644 --- a/src/_backend/swim-libraries.md +++ b/src/_backend/swim-libraries.md @@ -9,7 +9,7 @@ redirect_from: - /reference/swim-libraries.html --- -Swim implements a complete, self-contained, distributed application stack in an embeddable software library. To develop server-side Swim apps, add the [swim-api](https://github.com/swimos/swim/tree/main/swim-java/swim-runtime/swim-host/swim.api) library to your Java project. To write a JavaScript client application, install the [@swim/core](https://github.com/swimos/swim/tree/main/swim-js/swim-core) library from npm. To build a web application, npm install the [@swim/ui](https://github.com/swimos/swim/tree/main/swim-js/swim-ui) and [@swim/ux](https://github.com/swimos/swim/tree/main/swim-js/swim-ux) libraries. Select one of the boxes below (or scroll down) to get started with Swim. +Swim implements a complete, self-contained, distributed application stack in an embeddable software library. To develop server-side Swim apps, add the [swim-api](https://github.com/swimos/swim/tree/main/swim-java/swim-runtime/swim-host/swim.api) library to your Java project. To write a JavaScript client application, install the [@swim/client](https://www.npmjs.com/package/@swim/client) library from NPM. Select one of the boxes below (or scroll down) to get started with Swim. + +```javascript +const client = new WarpClient(); +const hvacDownlink = client + .downlink({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "hvac", + relinks: true, // will automatically attempt to reconnect after a network failure + syncs: false, // will not sychronize with remote lane state when opened; updates only + }); +``` + +## Opening a Downlink + +The `open` method is used to open a downlink after it has been configured. The downlink is returned. Data will not begin streaming through the downlink until it has been opened. The `close` method closes a downlink. + +```javascript +const client = new WarpClient(); +const downlink = client + .downlink({ + hostUri: "warp://example.com", + nodeUri: "/hotel/floor/1", + laneUri: "status" + }) + .open(); + +downlink.close(); +``` + +Closing a downlink does not necessarily close the underlying WARP link. The WARP client will keep a link open so long as at least one downlink to a given node and lane URI remains open. This prevents application components from stepping on each other's toes when they link to the same lanes of the same Web Agents. This can happen, for example, when a UI has a summary view and a detail view both display information derived from the same remote lane. The WARP link should not be closed when a detail view is hidden, if state updates are still required by the summary view. Events should also not be sent twice: once for the summary view, and once for the detail view. Neither the summary view nor the detail view should have to know about each other. And no global event dispatcher should be required, which could introduce consistency problems. WARP clients efficiently, and transparently handle all of these cases on behalf of all downlinks. + +## Downlink State and Lifecycle Callbacks + +A number of methods are made available for retrieving key pieces of a downlink's state. Optional callbacks may also be registered for reacting to changes in these states or other key lifecycle events. Callbacks may be included in the options object passed when creating a downlink, or set individually after a downlink has been initialized. + +The `connected` method returns true if the underlying connection to the remote host is currently open. `didConnect` registers an observer callback that gets invoked whenever a successful connection is made. Likewise, `didDisconnect` registers an observer callback which gets invoked when a connection has ended. + +The `linked` method returns true if the logical WARP link is currently open. Changes to the link's state may be observed by registering callbacks with `willLink`, `didLink`, `willUnlink`, or `didUnlink`. + +The `synced` method returns true if the WARP link is currently synchronized with the state of the remote lane. Users may observe synchronization using the `willSync` and `didSync` methods. + +The `opened` method returns true if the downlink has been opened. This is not necessarily always the same value as `linked`. Providing a downlink with an invalid hostUri, for example, could result in `opened` returning true and `linked` returning false. `didClose` gets invoked when `close` is called on a downlink. + +The `authenticated` method returns true if the underlying connection to the remote host is currently authenticated. + +And finally, all downlinks support registering `onEvent` and `onCommand` callbacks. The `onCommand` method accepts a callback used for observing outgoing command messages. The `onEvent` method registers a callback for observing all incoming events. `onEvent` is the rawest form of handling WARP messages. In most cases, users will be better off handling incoming updates with specialized observer callbacks defined on downlink subtypes because they provide better context about the type of message being received, allowing us to handle them more appropriately. More on this in later sections. diff --git a/src/_frontend/eventDownlink.md b/src/_frontend/eventDownlink.md new file mode 100644 index 0000000..625823a --- /dev/null +++ b/src/_frontend/eventDownlink.md @@ -0,0 +1,38 @@ +--- +title: Event Downlink +short-title: Event Downlink +description: "A WARP connection which provides a raw view of a WARP link. It receives all updates but is not purpose-built for a specific lane type." +group: Connections +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +`EventDownlink` is not so much a subtype of `Downlink` as it is the base type of all downlinks. While not literally a superclass of `ValueDownlink`, `MapDownlink`, and `ListDownlink`, `EventDownlink` inherits from the same prototype as the others but contains no additional frills. For example, `MapDownlink` and `ListDownlink` support registering different callbacks for observing when a key-value pair has been added versus when one has been removed; and `ValueDownlink` has the `didSet` callback for when its value has been updated. `EventDownlink`, on the other hand, offers no specialized handling of WARP messages with respect to the type of Web Agent lane it is connected to. It provides a raw view of a WARP link, passing all received messages to a single `onEvent` callback. + +Create an EventDownlink with a WARP client's `downlink` method. + +```typescript +import { WarpClient } from "@swim/client"; + +const downlink = client.downlink({ + hostUri: "warp://example.com", + nodeUri: "/house", + laneUri: "power/meter", +}) +.open(); +``` + +Using the `onEvent` callback, an application may update UI views and other dependent components in response to any messages received from the Web Agent. + +```typescript +downlink.onEvent = (event) => { + // update UI view with latest value + document.getElementById("status").innerText = `The house has used ${event.get("powerConsumption").numberValue()} kWh.`; +}; +``` + +## Typescript + +The format of an EventDownlink's state is unconstrained, therefore, EventDownlink may not be passed any [**Forms**]({% link _frontend/form.md %}) and type annotation via that route is not supported. Any necessary typechecking must be done ad hoc within the `onEvent` callback. diff --git a/src/_frontend/form.md b/src/_frontend/form.md new file mode 100644 index 0000000..62baa60 --- /dev/null +++ b/src/_frontend/form.md @@ -0,0 +1,65 @@ +--- +title: Form +short-title: Form +description: "A class which may be used for providing type information to downlinks." +group: Data +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +A Form defines a conversion between a structural type, and some nominal JavaScript type. The `mold` method converts a nominal JavaScript type to an Item. And the `cast` method converts an Item to a nominal JavaScript type, if possible. + +Here is an example of a custom Form which defines how the conversion of an entity representing a stock might look. Custom Forms should extend `Form`, which can be imported from `@swim/structure`. This library should automatically be present in your bundle as a dependency of `@swim/client`. + +```typescript +import { Form, Item } from "@swim/structure"; + +type Stock = { + symbol: string; + price: number; + volume: number; + dailyChange: number; +}; + +export class StockForm extends Form { + constructor() { + super(); + } + + // Item to JS object + override cast(item: Item): Stock | undefined { + if ( + item.tag === "update" && // make sure message is an update + item.getAttr("update").get("key").stringValue("") && // find key + item.get("price").isDefinite() && // ensure all fields are present + item.get("volume").isDefinite() && + item.get("movement").isDefinite() + ) { + return { + symbol: item.getAttr("update").get("key").stringValue(""), + price: item.get("price").numberValue(0), + volume: item.get("volume").numberValue(0), + dailyChange: item.get("dailyChange").numberValue(0), + }; + } + + // return undefined for all messages which don't fit the expected format + return undefined; + } + + // JS object to Item + override mold(object: Stock, item?: Item): Item { + let result = Item.fromLike(object); + if (item !== void 0) { + result = item.concat(object); + } + return result; + } +} +``` + +A downlink which expects to receive data matching the format of a Stock may now use this Form to coerce the Values it receives into a strongly typed `Stock` objects. If the Form receives data in an unrecognized format, it will return undefined, which is also part of its type. Remember that [`isDistinct`](/frontend/structures#isDistinct) is used to check that a `Value` is not [`Extant`](/frontend/structures#unit-value-types) or [`Absent`](/frontend/structures#unit-value-types), and [`isDefinite`](/frontend/structures#isDefinite) checks that it's not `false` or an empty [`Record`](/frontend/structures#composite-value-type) either. + +The example above demonstrates a straightforward use case of converting expected structural types into typed JavaScript objects more comfortable to work with. The possibilites afforded by custom Forms, however, go beyond simple conversion. Further transformation of the data could be performed within Forms as well. While it would likely be cleaner and more efficient to do such operations from inside of a Web Agent (as that is one of their defining purposes), it is wholly possible to do data transformation within a Form as well if one were so inclined. diff --git a/src/_frontend/gettingStarted.md b/src/_frontend/gettingStarted.md new file mode 100644 index 0000000..6480445 --- /dev/null +++ b/src/_frontend/gettingStarted.md @@ -0,0 +1,119 @@ +--- +title: Getting Started +short-title: Getting Started +description: "Steps for connecting to a SwimOS application and building UIs powered by streaming data" +group: Introduction +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +Swim is a full stack **streaming application platform** for building stateful services, streaming APIs, and real-time UIs. Streaming applications push differential state changes through the full application stack, eliminating the need for polling and streaming only what each client chooses to observe. Real-time UIs render live views of distributed application state. + +Creating a real-time UI starts with [**WARP Client**]({% link _frontend/warpClient.md %}), a streaming API client for consuming multiplexed streaming APIs. It opens links to lanes of stateful Web Agents using the [**WARP**]({% link _frontend/whatIsWarp.md %}) protocol, enabling massively real-time applications that continuously synchronize all shared states with half ping latency. The client requires no configuration and makes opening links to Web Agents a cinch. It is UI framework-agnostic and works in both browser and Node.js runtime environments. + +## Installation + +To begin using WARP client, install the `@swim/client` package. + +**NPM** +```bash +npm install @swim/client@dev +``` + +**CDN** + +```html + + + + + +``` + +## Usage + +**NPM** + +Exports of `@swim/client` may be imported as ES modules in ES2015-compatible environments. You may also import modules from a number of other Swim libraries installed as dependencies of `@swim/client`. A notable example of this is `@swim/structure` which we will see referenced in later sections. + +```typescript +import { WarpClient } from "@swim/client"; +import { Value } from "@swim/structure"; +``` + + + +**Browser / CDN** + +The swim.js script bundles exports from `@swim/client`, its Swim dependencies, and a number of additional Swim libraries useful for building real time UIs, including tools for making charts and maps. When loaded by a web browser, the swim.js script adds all library exports to the global `swim` namespace. + +```javascript +const client = new swim.WarpClient(); +``` + +**TypeScript** + +Typescript definition files are provided in the libraries. + +All downlink variants support the ability to provide type information for incoming and outgoing WARP messages. The exception to this is [EventDownlink]({% link _frontend/eventDownlink.md %}), which should rarely be used. See our article on [**Forms**]({% link _frontend/form.md %}) for more details and an example of how to provide downlinks with type information. + + + +## Quick Start + +Connecting to a remote Web Agent with the WARP client can be done in just a few lines. + +Import and initialize an instance of `WarpClient`. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +``` + +Next, create a link for connecting to your remote Web Agent. + +```typescript +const downlink = client.downlink(); +``` + +Then provide your link with the URI of the Web Agent to which you wish to connect. + +```typescript +downlink.setHostUri("warp://example.com"); +downlink.setNodeUri("/myAgent"); +downlink.setLaneUri("someLane"); +``` + +And define a callback for handling messages received by the link. + +```typescript +downlink.onEvent = (value) => { + const userCount = `${value.stringValue("0")} Users`; + document.getElementById("active-user-count").innerText = userCount; +}; +``` + +Finally, open your downlink. + +```typescript +downlink.open(); +``` + +Once the downlink is open, events should begin streaming into the application. Whether they arrive as a trickle or a flood, applications may use these messages to update UI views and other dependent components, keeping them consistent with the shared state of the remote Web Agent in network real-time. diff --git a/src/_frontend/index.md b/src/_frontend/index.md new file mode 100644 index 0000000..2c09b77 --- /dev/null +++ b/src/_frontend/index.md @@ -0,0 +1,9 @@ +--- +title: Frontend +permalink: /frontend/ +layout: documentation +description: "Frontend SwimOS" +toc: false +--- + +{% include docs-listing.html %} diff --git a/src/_frontend/listDownlink.md b/src/_frontend/listDownlink.md new file mode 100644 index 0000000..3234c04 --- /dev/null +++ b/src/_frontend/listDownlink.md @@ -0,0 +1,90 @@ +--- +title: List Downlink +short-title: List Downlink +description: "A WARP connection which synchronizes a shares real-time list with a remote list lane. It behaves similar to a JavaScript array." +group: Connections +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +A ListDownlink synchronizes a shared real-time list with a remote list lane. In addition to the standard Downlink callbacks, ListDownlink supports registering `willUpdate`, `didUpdate`, `willMove`, `didMove`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked list state — whether remote or local. + +Create a ListDownlink with a WARP client's `downlinkList` method. + +ListDownlink behaves similarly to a JavaScript array. Use the ListDownlink.`get` method to get the item at a given index. Use the ListDownlink.`set` method to update the item at some index. And use the ListDownlink.`splice` method to insert and remove items from the list. You can also `push`, `pop`, `shift`, and `unshift` items, and move an item from one index to another. + +```typescript +import { WarpClient } from "@swim/client"; + +const listDownlink = client.downlinkList({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "reservations" +}) +.open(); + +listDownlink.get(0); // get the first item in the list +listDownlink.set(0, { arrival: 1707956415650, nights: 1, guestName: "Jeff Lebowski" }); // locally and remotely update an item +listDownlink.push({ arrival: 1707748815650, nights: 2, guestName: "Walter Sobchak" }); // locally and remotely append an item +``` + +For the most part, client code can treat a ListDownlink like an ordinary JavaScript list; the WARP client will ensure that the downlink is continuously made consistent with the remote lane. Using `didUpdate`, `didMove`, and `didRemove` callbacks, applications can update UI list views and other dependent components to keep them consistent with the shared state of the remote list lane in network real-time. + +```typescript +const list = client.downlinkList({ + didUpdate: (index, value) => { + if (hasChildElement(index)) { + // update existing UI view at index + } else { + // insert new UI view at index + } + }, + didMove: (fromIndex, toIndex, value) => { + // move existing UI view from old index to new index + }, + didRemove: (index) => { + // remove UI view at index + } +}) +``` + +## State Type Disambiguation + +A ListDownlink views its items as [**Values**]({% link _frontend/structures.md %}) by default. Use the valueForm method to create a typed projection of a ListDownlink that automatically transforms its items using a [**Form**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ListDownlink's state to a boolean; and you can also use `Form.forAny()` to create a ListDownlink that coerces its state to a plain old JavaScript value. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. + +```typescript +import { Form } from "@swim/structure"; + +const list = client.downlinkList({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "reservation", + valueForm: Form.forAny(), + didUpdate: (index, value) => {/* ... */}, + didMove: (fromIndex, toIndex, value) => {/* ... */}, + didRemove: (index) => {/* ... */}, +}) +.open(); +``` + +## Typescript + +ListDownlink state may also be given a type annotation. All that is required is for a custom Form to be provided to the `valueForm` option. See our article on [**Forms**]({% link _frontend/form.md %}) for an example on how to do this. + +```typescript +const list = client.downlinkList({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "reservation", + valueForm: new ReservationForm(), + didUpdate: (index, value) => {/* ... */}, + didMove: (fromIndex, toIndex, value) => {/* ... */}, + didRemove: (index) => {/* ... */}, +}) +.open(); + +// typed Reservation object +list.get(0); // { arrival: 1707748815650, nights: 2, guestName: "Walter Sobchak" } +``` diff --git a/src/_frontend/mapDownlink.md b/src/_frontend/mapDownlink.md new file mode 100644 index 0000000..d60a3c9 --- /dev/null +++ b/src/_frontend/mapDownlink.md @@ -0,0 +1,83 @@ +--- +title: Map Downlink +short-title: Map Downlink +description: "A WARP connection which synchronizes a shares real-time, key-value map with a remote map lane" +group: Connections +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +A MapDownlink synchronizes a shared real-time key-value map with a remote map lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. + +Create a MapDownlink with a WARP client's `downlinkMap` method. + +MapDownlink implements the standard JavaScript Map interface. Use the `get` method to get the value associated with a given key. Use the `set` method to update the value associated with a key. And use the `delete` method to remove a key and its associated value. + +```typescript +import { WarpClient } from "@swim/client"; + +const mapDownlink = client.downlinkMap({ + hostUri: "warp://example.com", + nodeUri: "/hotel/lobby", + laneUri: "elevators" +}) +.open(); +mapDownlink.get("guest"); // get the locally cached value associated with the key +mapDownlink.set("service", newElevator); // locally and remotely insert a new entry +mapDownlink.delete("parking"); // locally and remotely remove an existing entry +``` + +For the most part, client code can treat a MapDownlink like an ordinary JavaScript Map; the WARP client will ensure that the downlink is continuously made consistent with the remote lane. Using `didUpdate` and `didRemove` callbacks, applications can update UI collection views and other dependent components to keep them consistent with the shared state of the remote map lane in network real-time. Callbacks may be provided as an option during downlink initialization or defined later on, as shown below. + +```typescript +mapDownlink.didUpdate = (key, value) => { + if (hasChildElement(key)) { + // update existing UI view for key + } else { + // insert new UI view for key + } +} +mapDownlink.didRemove((key) => { + // remove UI view for key +}) +``` + +## State Type Disambiguation + +A MapDownlink views its keys and values as [**Values**]({% link _frontend/structures.md %}) by default. Use the `keyForm` and `valueForm` methods to create a typed projection of a MapDownlink that automatically transforms its keys and values using [**Forms**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ValueDownlink's state to a boolean; and you can also use `Form.forAny()` to create a ValueDownlink that coerces its state to a plain old JavaScript value. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. + +```typescript +const elevators = client.downlinkMap({ + hostUri("warp://example.com"), + nodeUri("/hotel/lobby"), + laneUri("elevators"), + keyForm(Form.forString()), + valueForm(Form.forAny()), + didUpdate((key, value) => /* ... */), + didRemove((key) => /* ... */), +}) +.open(); +``` + +## Typescript + +MapDownlink state may also be given a type annotation. All that is required is for a custom Form to be provided to the `valueForm` option. See our article on [**Forms**]({% link _frontend/form.md %}) for an example on how to do this. + +```typescript +import { WarpClient } from "@swim/client"; +import { Form } from "@swim/structure"; + +const elevators = client.downlinkValue>({ + hostUri: "warp://example.com", + nodeUri: "/hotel/lobby", + laneUri: "elevators", + keyForm: Form.forString(), + valueForm: new ElevatorForm(), +}) +.open(); + +// typed Elevators object +elevators.get("guest"); // { id: 12345, currentFloor: 1, occupied: false, lastInspection: 1707216815650 } +``` diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md new file mode 100644 index 0000000..69b5e3c --- /dev/null +++ b/src/_frontend/structures.md @@ -0,0 +1,189 @@ +--- +title: Structures +short-title: Structures +description: "The structures which constitute the data model within @swim/structure; starting with Item and continuing on to its subclasses: Field, Attr, Slot, and Value." +group: Data +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +## Item + +At the center of @swim/structure is the `Item` class, which defines an algebraic data type for representing and manipulating structured data. Item provides many methods for operating on structured values, most of which are closed over the Item type, meaning they always return other instances of Item. This closure of operations over the Item type makes it safe and expressive to traverse, transform, and convert arbitrary data structures, without excessive conditional logic to type check and validate structures obtained from external sources. + +Every `Item` is either a Field or a Value. Every Field is either an Attr or a Slot. And every Value is either a Record, Data, Text, Num, Bool, Extant, or Absent. Think of Item as analogous to the set of all JSON values, with the inclusion of object fields as first class elements. + +
+ Swim data structures; Item, Field, and Value +
+ +## Field + +A `Field` represents a key-value pair, where both the key and value are of type Value. An `Attr` is a discriminated kind of Field whose key is always of type Text. Every Field that is not explicitly an Attr is a `Slot`. Think of a Slot as a field of a JSON object, or as an attribute of an XML tag. Think of an Attr like an XML tag, where the key of the Attr is the tag name, and the value of the Attr is a Record containing the element's attributes. + +## Value + +Every Item that is not a Field is a `Value`. A Value can either be one of four primitive value types: Data, Text, Num, or Bool; one of two unit types: Extant, or Absent; or the composite type: Record. Think of a Value as representing an arbitrary data structure. + +### Primitive Value Types + +A `Data` object represents opaque binary data; it wraps a JavaScript Uint8Array. A `Text` object represents a Unicode string, and wraps a primitive JavaScript string. A `Num` object represents a numeric value, encapsulating a primitive JavaScript number. A `Bool` object represents a boolean value, wrapping a primitive JavaScript boolean. + +### Unit Value Types + +There are two unit types: `Extant`, and `Absent`. Extant represents a thing that exists, but has no value; sort of like JavaScript's null value, but a valid object on which you can invoke methods. Absent represents something that does not exist; similar to JavaScript's undefined value, but a valid instance of Item. + +### Composite Value Type + +A `Record` is a simple container of Item members, and is the only composite structure type. A Record containing only Field members is analogous to a JSON object—though unlike JSON, its keys are not restricted to strings. A Record containing only Value members is similar to a JSON array. A Record with a leading Attr bears resemblance to an XML element. And a Record with a mixture of Field and Value members acts like a partially keyed list. + +## Item Reference + +Since everything is an Item, each of these methods are available on every kind of structure. + +`isDefined(): boolean;` + +Returns _true_ if this _Item_ is not _Absent_. + +`isDistinct(): boolean;` + +Returns _true_ if this _Item_ is neither _Extant_ nor _Absent_. + +`isDefinite(): boolean;` + +Returns _true_ if this _Item_ is not one of: an empty _Record_, _False_, _Extant_, or _Absent_. + +`isConstant(): boolean;` + +Returns _true_ if this _Item_ always _evaluates_ to the same _Item_. + +`readonly key: Value;` + +Returns the key component of this _Item_, if this _Item_ is a Field; otherwise returns Absent if this _Item_ is a _Value_. + +`toValue(): Value;` + +Returns the value component of this _Item_, if this _Item_ is a Field; otherwise returns _this_ if this _Item_ is a _Value_. + +`readonly tag: string | undefined;` + +Returns the _key_ string of the first member of this _Item_, if this _Item_ +is a Record, and its first member is an Attr; otherwise returns +_undefined_ if this _Item_ is not a _Record_, or if this _Item_ is a _Record_ whose first member is not an _Attr_. +Used to concisely get the name of the discriminating attribute of a structure. The _tag_ can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag. + +`readonly target: Value;` + +Returns the _flattened_ members of this _Item_ after all +attributes have been removed, if this _Item_ is a Record; otherwise returns _this_ if this _Item_ is a _non-Record_ _Value_, or returns the value component if this _Item_ is a _Field_. +Used to concisely get the scalar value of an attributed structure. An attributed structure is a _Record_ with one or more attributes that modify one or more other members. + +`flattened(): Value;` + +Returns the sole member of this _Item_, if this _Item_ is a Record +with exactly one member, and its member is a _Value_; returns Extant +if this _Item_ is an empty _Record_; returns Absent if this _Item_ is +a _Field_; otherwise returns _this_ if this _Item_ is a _Record_ with more +than one member, or if this _Item_ is a _non-Record_ _Value_. +Used to convert a unary _Record_ into its member _Value_. Facilitates +writing code that treats a unary _Record_ equivalently to a bare _Value_. + +`unflattened(): Record;` + +Returns _this_ if this _Item_ is a Record; returns a _Record_ +containing just this _Item_, if this _Item_ is _distinct_; otherwise returns an empty _Record_ if this _Item_ is Extant or Absent. Facilitates writing code that treats a bare _Value_ equivalently to a unary _Record_. + +`header(tag: string): Value;` + +Returns the value of the first member of this _Item_, if this _Item_ is a +Record, and its first member is an Attr whose _key_ string is equal to _tag_; otherwise returns Absent if this _Item_ is not a _Record_, or if this _Item_ is a _Record_ whose first member is not an _Attr_, or if this _Item_ is a _Record_ whose first member is an _Attr_ whose _key_ does not equal the _tag_. Used to conditionally get the value of the head _Attr_ of a structure, if and only if the key string of the head _Attr_ is equal to the _tag_. Can be used to check if a structure might conform to a nominal type named _tag_, while simultaneously getting the value of the _tag_ attribute. + +`headers(tag: string): Record | undefined;` + +Returns the _unflattened_ _header_ of +this _Item_, if this _Item_ is a Record, and its first member is an Attr whose _key_ string is equal to _tag_; otherwise returns _undefined_. The _headers_ of the _tag_ attribute of a structure are like the attributes of an XML element tag; through unlike an XML element, _tag_ attribute headers are not limited to string keys and values. + +`head(): Item;` + +Returns the first member of this _Item_, if this _Item_ is a non-empty +Record; otherwise returns Absent. + +`tail(): Record;` + +Returns a view of all but the first member of this _Item_, if this _Item_ +is a non-empty _Record_; otherwise returns an empty _Record_ if this _Item_ is not a _Record_, or if this _Item_ is itself an empty _Record_. + +`body(): Value;` + +Returns the _flattened_ _tail_ of this +_Item_. Used to recursively deconstruct a structure, terminating with its last _Value_, rather than a unary _Record_ containing its last value, if the structure ends with a _Value_ member. + +`readonly length: number;` + +Returns the number of members contained in this _Item_, if this _Item_ is +a Record; otherwise returns _0_ if this _Item_ is not a _Record_. + +`has(key: ValueLike): boolean;` + +Returns _true_ if this _Item_ is a Record that has a Field member +with a key that is equal to the given _key_; otherwise returns _false_ if this _Item_ is not a _Record_, or if this _Item_ is a _Record_, but has no _Field_ member with a key equal to the given _key_. + +`get(key: ValueLike): Value;` + +Returns the value of the last Field member of this _Item_ whose key +is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Field_ member with a key equal to the given _key_. + +`getAttr(key: TextLike): Value;` + +Returns the value of the last Attr member of this _Item_ whose key +is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Attr_ member with a key equal to the given _key_. + +`getSlot(key: ValueLike): Value;` + +Returns the value of the last Slot member of this _Item_ whose key +is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Slot_ member with a key equal to the given _key_. + +`getField(key: ValueLike): Field | undefined;` + +Returns the last Field member of this _Item_ whose key is equal to the +given _key_; returns _undefined_ if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Field_ member with a _key_ equal to the given _key_. + +`getItem(index: NumLike): Item;` + +Returns the member of this _Item_ at the given _index_, if this _Item_ is +a Record, and the _index_ is greater than or equal to zero, and less than the _length_ of the _Record_; otherwise returns Absent if this _Item_ is not a _Record_, or if this _Item_ is a _Record_, but the _index_ is out of bounds. + +`evaluate(interpreter: InterpreterLike): Item;` + +Returns a new _Item_ with all nested expressions interpreted in lexical order and scope. + +`stringValue(): string | undefined;` +Converts this _Item_ into a _string_ value, if possible; otherwise returns +_undefined_ if this _Item_ can't be converted into a _string_ value. + +`stringValue(orElse: T): string | T;` +Converts this _Item_ into a _string_ value, if possible; otherwise returns +_orElse_ if this _Item_ can't be converted into a _string_ value. + +`numberValue(): number | undefined;` +Converts this _Item_ into a _number_ value, if possible; otherwise returns +_undefined_ if this _Item_ can't be converted into a _number_ value. + +`numberValue(orElse: T): number | T;` +Converts this _Item_ into a _number_ value, if possible; otherwise returns +_orElse_ if this _Item_ can't be converted into a _number_ value. + +`booleanValue(): boolean | undefined;` +Converts this _Item_ into a _boolean_ value, if possible; otherwise returns +_undefined_ if this _Item_ can't be converted into a _boolean_ value. + +`booleanValue(orElse: T): boolean | T;` +Converts this _Item_ into a _boolean_ value, if possible; otherwise returns +_orElse_ if this _Item_ can't be converted into a _boolean_ value. + +`readonly typeOrder: number;` +Returns the heterogeneous sort order of this _Item_. Used to impose a +total order on the set of all items. When comparing two items of +different types, the items order according to their _typeOrder_. diff --git a/src/_frontend/valueDownlink.md b/src/_frontend/valueDownlink.md new file mode 100644 index 0000000..5607379 --- /dev/null +++ b/src/_frontend/valueDownlink.md @@ -0,0 +1,82 @@ +--- +title: Value Downlink +short-title: Value Downlink +description: "A WARP connection which synchronizes a shares real-time, scalar value with a remote value lane" +group: Connections +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +A ValueDownlink synchronizes a shared real-time value with a remote value lane. In addition to the standard Downlink callbacks, ValueDownlink supports registering `willSet` and `didSet` callbacks to observe all changes to downlinked state — whether remote or local. + +Create a ValueDownlink with a WARP client's `downlinkValue` method. + +Use the `get` method to get the current state value, and the `set` method to set the current state value. For the most part, client code can treat a ValueDownlink like an ordinary mutable variable; the WARP client will ensure that the downlink is continuously made consistent with the remote lane. + +```typescript +import { WarpClient } from "@swim/client"; + +valueDownlink = client.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "light" +}) +.open(); +valueDownlink.get(); // get the current local state of the downlink +valueDownlink.set(newValue); // update the local and remote state of the downlink +``` + +Using `didSet` callbacks, applications can update UI views and other dependent components to keep them consistent with the shared state of the remote value lane in network real-time. The `didSet` callback may be provided as an option during downlink initialization or defined later on, as shown below. + +```typescript +valueDownlink.didSet = (newValue) => { + // update UI view with latest value + document.getElementById("status").innerText = `Kitchen light is ${newValue.get("isOn") ? "on" : "off"}.`; +}; +``` + +## State Type Disambiguation + +A ValueDownlink views its state as a [**Value**]({% link _frontend/structures.md %}) by default. Use the `valueForm` option to create a typed projection of a ValueDownlink that automatically transforms its state using a [**Form**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ValueDownlink's state to a boolean; and you can also use `Form.forAny()` to create a ValueDownlink that coerces its state to a plain old JavaScript value. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. + +```typescript +const light = client.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "tableLamp", + valueForm: Form.forBoolean(), +}) +.open(); + +light.get(); // true + + +const frontDoor = client.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "door", + valueForm: Form.forAny(), +}) +.open(); + +frontDoor.get(); // { locked: true, lastActivity: 1708030692845 } +``` + +## Typescript + +ValueDownlink state may also be given a type annotation. All that is required is for a custom Form to be provided to the `valueForm` option. See our article on [**Forms**]({% link _frontend/form.md %}) for an example on how to do this. + +```typescript +const frontDoor = client.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "/hotel/room/123", + laneUri: "lastEntrant", + valueForm: new KeyCardForm(), +}) +.open(); + +// typed KeyCard object +frontDoor.get(); // { id: 123456, issueAt: 1708030692845, authorizedUntil: 1708032815650 } +``` diff --git a/src/_frontend/warpClient.md b/src/_frontend/warpClient.md new file mode 100644 index 0000000..4abe0e4 --- /dev/null +++ b/src/_frontend/warpClient.md @@ -0,0 +1,192 @@ +--- +title: WarpClient +short-title: WarpClient +description: "The go-to starting point for most data streaming and connection management use cases" +group: Connections +layout: documentation +redirect_from: +--- + +_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ + +**WarpClient** is the class which behaves as the primary mechanism for handling connection management and link routing. WARP clients transparently multiplex all links to [**Web Agents**]({% link _backend/web-agents.md %}) on a given host over a single WebSocket connection, and automatically manage the network connection to each host, including reconnection and resynchronization after a network failure. Key lifecycle events may also be observed through the registration of callbacks. + +Besides managing connections and opening links (from here on called [**downlinks**]({% link _frontend/downlinks.md %})) to Web Agents, WARP clients do many other things. They can be used to send arbitrary WARP commands, provide authentication credentials for hosts, and create HostRef, NodeRef, and LaneRef scopes to facilitate downlink management. Additionally, when multiple downlinks are opened to the same lane of the same remote Web Agent, WARP clients seamlessly handle multicast event routing. + +## Instantiating a WarpClient + +`WarpClient`'s constructor requires no arguments. + +```javascript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +``` + +A singleton is also available via the `.global()` getter method. + +```javascript +import { WarpClient } from "@swim/client"; + +const globalClient = WarpClient.global(); +``` + +## Opening Downlinks + +A [**downlink**]({% link _frontend/downlinks.md %}) provides a virtual bidirectional stream over which data can be synchronized between the client and a lane of a remote Web Agent. WARP clients transparently multiplex all links to Web Agents on a given host over a single WebSocket connection. A downlink represents one link in this scenario. + +`WarpClient` includes four methods that open different kinds of downlinks. The `downlink` method creates an EventDownlink for streaming raw events from any Web Agent lane. The `valueDownlink` method creates a ValueDownlink for synchronizing state with a Web Agent value lane. The `mapDownlink` method creates a MapDownlink for synchronizing state with a Web Agent map lane. And the `listDownlink` method creates a ListDownlink for synchronizing state with a Web Agent list lane. + +Here is an example of opening an EventDownlink. We will go into further detail on all of the downlink types in subsequent sections. + +```javascript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +const downlink = client + .downlink({ + hostUri: "warp://example.com", + nodeUri: "/building/1", + laneUri: "status" + }) +``` + +## Observing Lifecycle Events + +`WarpClient` instances can also be used to observe key lifecycle events. The `didConnect` method registers an observer callback that gets invoked whenever a connection to a WARP host is established. The `didDisconnect` method registers an observer callback that gets invoked whenever a WARP host disconnects. `didAuthenticate` registers an observer callback that gets invoked whenever the client successfully authenticates with a WARP host. The `didDeauthenticate` method gets invoked when a WARP host rejects the client's authentication credentials. And `didFail` registers an observer callback that gets invoked when the client encounters an unexpected error. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +client.hostDidConnect = (host) => { + console.log("connected to", host); +} +client.hostDidDisconnect = (host) => { + console.log("disconnected from", host); +} +client.hostDidAuthenticate = (session, host) => { + console.log("authenticated to", host, "with session", session.toLike()); +} +client.hostDidDeauthenticate = (reason, host) => { + console.log("deauthenticated from", host, "because", reason.toLike()); +} +client.hostDidFail = (error, host) => { + console.log("host", host, "failed because", error); +} +``` + +## Authentication + +The `authenticate` method associates a credentials structure with a particular host URI. The credentials will be sent in a WARP @auth envelope whenever the client connects to the specified host. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +client.authenticate("warps://example.com", {"@openId": jwt}); +``` + +Distinct `WarpClient` instances can be used to create isolated connection pools for different security domains. + +```typescript +import { WarpClient } from "@swim/client"; + +const userClient = new WarpClient(); +userClient.authenticate("warps://example.com", {"@openId": userJwt}); + +const toolClient = new WarpClient(); +toolClient.authenticate("warps://example.com", {"@oauth": toolJwt}); +``` + +## Sending Commands + +The `command` method sends a WARP command message to a lane of a remote node. `command` takes up to four arguments: a host URI, a node URI, a lane URI, and a command payload. The URIs may be ommitted if they are already set on the `WarpClient`, though a command payload is always required. A URI with more specificity may not be ommitted if a less specific one is included. For example, the first two commands below are valid while the third is not. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +client.command("warp://example.com", "/house/kitchen", "light", "off"); // valid + +client.hostUri.set("warp://example.com"); +client.command("/house/kitchen", "light", "off"); // valid + +client.laneUri.set("light"); +client.command("/house/kitchen", "off"); // invalid +``` + +## Refs + +`Refs` are a useful tool for grouping and organizing downlinks. Still capable of sending commands, providing authentication credentials, and opening downlinks, `Refs` can be thought of as a particular scope of a WarpClient instance, and where it stores a subset of its links. This analogy of scope is accurate as the `Refs` are actually instances of a class called `WarpScope`. `Refs` must have a portion of their address pre-configured — at minimum the `hostUri`, and optionally the `nodeUri` and `laneUri`. When downlinks are opened from a `Ref` they are bound to the portion of the address provided. + +### HostRef + +A `HostRef` only needs a `hostUri` to be initialized. + +```typescript +import { WarpClient } from "@swim/client"; + +const hostRef = client.hostRef("warp://example.com"); +hostRef.downlink({ + nodeUri: "house/kitchen", + laneUri: "light" +}) +.open(); +``` + +The `HostRef.nodeRef` and `HostRef.laneRef` instance methods can be used to create further resolved WARP scopes. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +const hostRef = client.hostRef("warp://example.com"); +const nodeRef = hostRef.nodeRef("/house/kitchen"); +const laneRef = nodeRef.laneRef("/house/kitchen", "light"); +``` + +### NodeRef + +A `NodeRef` needs a `hostUri` and a `nodeUri` to be initialized. + +```typescript +const nodeRef = client.nodeRef("warp://example.com", "/house/kitchen"); +nodeRef.downlink({ laneUri: "light" }).open(); +``` + +The `NodeRef.laneRef` instance method can be used to create further resolved WARP scopes. + +```typescript +const nodeRef = client.nodeRef("warp://example.com", "/house/kitchen"); +const laneRef = nodeRef.laneRef("light"); +``` + +### LaneRef + +A `LaneRef` needs all three of `hostUri`, `nodeUri`, and `laneUri` to be initialized. + +```typescript +const laneRef = client.laneRef("warp://example.com", "/house/kitchen", "light"); +laneRef.downlink().open(); +``` + +## Utility Methods + +### isOnline + +The `isOnline` method returns true when the the client has access to a network; it can also be used to force a client online or offline. The WarpClient.keepOnline method controls whether or not the client should automatically reopen connections after a network failure. Note that the keepOnline state of the client overrides the keepLinked state of individual downlinks. Setting keepOnline to false can be useful for ephemeral clients, but should typically be left true. + +```typescript +import { WarpClient } from "@swim/client"; + +const client = new WarpClient(); +client.isOnline(); // true most of the time + +client.isOnline(false); // force offline +client.isOnline(true); // force online + +client.keepOnline(); // defaults to true + +client.keepOnline(false); // disable network reconnection +``` diff --git a/src/_frontend/whatIsWarp.md b/src/_frontend/whatIsWarp.md new file mode 100644 index 0000000..4611613 --- /dev/null +++ b/src/_frontend/whatIsWarp.md @@ -0,0 +1,24 @@ +--- +title: What is WARP? +short-title: What is WARP? +description: "An introduction to the protocol which serves as the basis for communication between Web Agents and browser clients. It enables multiplexing bidirectional streams between large numbers of URIs over a single WebSocket connection" +group: Introduction +layout: documentation +redirect_from: +--- + +## Introduction + +The internet routes packets between physical or virtual machines, identified by IP addresses. On top of the internet sits the World Wide Web you're using right now, which routes requests and responses for Web Resources, identified by URIs. The Web was built for documents. But treating everything like a document is quite limiting. A smart city isn't a document; it's a continuously evolving digital mirror of reality. + +Things that continuously evolve — like smart cities, autonomous control systems, and collaborative applications — are best modeled as streams of state changes. We can't — and don't want to — give every logical thing in the world an IP address; we want to give things logical names, i.e. URIs. It would be nice if we had a way to route packets between URIs — like an application layer network. + +In recent years, the WebSocket protocol has emerged, enabling us to open streaming connections to URIs. This is great. But it only solves part of the problem. Applications need to open a new WebSocket connection for every URI to which they want to connect. This limitation prevents WebSockets alone from serving as a streaming layer of the World Wide Web. But that's OK; that's not WebSocket's role. + +## WARP + +WARP is a protocol for multiplexing bidirectional streams between large numbers of URIs over a single WebSocket connection. Multiplexed streams within a WARP connection are called **links**. Just as the World Wide Web has hypertext links between Web Pages, WARP enables actively streaming links between [**Web Agents**]({% link _backend/web-agents.md %}) and a browser client or another Web Agent. WARP is like pub-sub without the broker, enabling every state of a Web API to be streamed, without interference from billions of queues. Whether you have a UI trying to display the most up-to-date state, or a Web Agent aggregating real-time data from multiple sources into something more meaningful, lighning fast two-way communication through multiple data streams is made possible over a single WebSocket connection with WARP. + +WARP connections exchange WebSocket frames encoded as [**Recon**]({% link _backend/recon.md %}) structures. The use of Recon enables efficient parsing and routing, while preserving the flexibility and extensibility of protocols like HTTP. And the expressiveness of Recon avoids the explosion of specialized grammars and parsers that has caused the originally simple and elegant HTTP protocol to become bloated and complex. It's also worth noting that, although HTTP/2 introduces a limited form of multiplexing, it multiplexes RPC calls, not full-duplex streams. + +Having a multiplexed streaming protocol allows a large number of links to be made without incurring significant additional overhead. It is an invaluable tool when building UIs which intend to engage in real-time data streaming. diff --git a/src/_tutorials/transit.md b/src/_tutorials/transit.md index 1420c80..fe255b0 100644 --- a/src/_tutorials/transit.md +++ b/src/_tutorials/transit.md @@ -520,7 +520,7 @@ Here's the HTML source: https://github.com/swimos/tutorial-transit/blob/main/ui/index.html -By including a few SwimOS libraries, the HTML made includes a tiny bit of javascript to reference the host, the Web Agent uri, and the specific lane. Through calls to `swim.HTMLView`, `swim.MapboxView`, and `swim.GeoTreeView`, the incoming, real-time data stream from SwimOS gets plotted directly against the map. +By including a few SwimOS libraries, the HTML made includes a tiny bit of JavaScript to reference the host, the Web Agent uri, and the specific lane. Through calls to `swim.HTMLView`, `swim.MapboxView`, and `swim.GeoTreeView`, the incoming, real-time data stream from SwimOS gets plotted directly against the map. You can find directions in the top-level README.md file to run the UI. @@ -573,4 +573,4 @@ which yields: ``` {id:"116",routeTag:CLEB,dirTag:CLEB_0_var0,lat:"34.083697",lon:"-118.387662",secsSinceReport:"6",predictable:true,heading:"322",speedKmHr:"11",uri:"/vehicle/west-hollywood/116"} {id:"116",routeTag:CLEB,dirTag:CLEB_0_var0,lat:"34.084492",lon:"-118.386684",secsSinceReport:"8",predictable:true,heading:"51",speedKmHr:"32",uri:"/vehicle/west-hollywood/116"} -``` \ No newline at end of file +``` From 24af381ca0a39f9373233f6ef0bfd08a72443757 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Tue, 27 Feb 2024 12:57:11 -0800 Subject: [PATCH 03/17] Advise using mapDownlinks for join value lanes --- src/_frontend/mapDownlink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/mapDownlink.md b/src/_frontend/mapDownlink.md index d60a3c9..d9a98ba 100644 --- a/src/_frontend/mapDownlink.md +++ b/src/_frontend/mapDownlink.md @@ -9,7 +9,7 @@ redirect_from: _This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ -A MapDownlink synchronizes a shared real-time key-value map with a remote map lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. +A MapDownlink synchronizes a shared real-time key-value map with a remote [**map lane**]({% link _backend/map-lanes.md }). MapDownlinks are also the downlink best suited for use with [**join value lanes**]. Much like a map lane, join value lanes are key-value maps where each value is itself a link to another value lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. Create a MapDownlink with a WARP client's `downlinkMap` method. From 27548a7101ac51d9b7ad8b81b4ea15a25e220e4f Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 28 Feb 2024 14:34:09 -0800 Subject: [PATCH 04/17] Rename startup script from launch to dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cb0ba6..8d73ee0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "postcss.config.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "launch": "bundle exec jekyll serve --livereload" + "dev": "bundle exec jekyll serve --livereload" }, "author": "", "license": "ISC", From 8e1979acfd02c97ffcbc5779f2820307c8d3c4fe Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 28 Feb 2024 15:30:24 -0800 Subject: [PATCH 05/17] Close liquid tag accidentally left open --- src/_frontend/mapDownlink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/mapDownlink.md b/src/_frontend/mapDownlink.md index d9a98ba..11095e8 100644 --- a/src/_frontend/mapDownlink.md +++ b/src/_frontend/mapDownlink.md @@ -9,7 +9,7 @@ redirect_from: _This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ -A MapDownlink synchronizes a shared real-time key-value map with a remote [**map lane**]({% link _backend/map-lanes.md }). MapDownlinks are also the downlink best suited for use with [**join value lanes**]. Much like a map lane, join value lanes are key-value maps where each value is itself a link to another value lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. +A MapDownlink synchronizes a shared real-time key-value map with a remote [**map lane**]({% link _backend/map-lanes.md %}). MapDownlinks are also the downlink best suited for use with [**join value lanes**]. Much like a map lane, join value lanes are key-value maps where each value is itself a link to another value lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. Create a MapDownlink with a WARP client's `downlinkMap` method. From 0c4d5d89f86d159d51f310a1e5d5608bed752238 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 28 Feb 2024 15:46:00 -0800 Subject: [PATCH 06/17] Use alert for not about Swim JS package versions --- src/_frontend/dataModel.md | 2 +- src/_frontend/downlinks.md | 2 +- src/_frontend/eventDownlink.md | 2 +- src/_frontend/form.md | 2 +- src/_frontend/gettingStarted.md | 2 +- src/_frontend/listDownlink.md | 2 +- src/_frontend/mapDownlink.md | 2 +- src/_frontend/structures.md | 2 +- src/_frontend/valueDownlink.md | 2 +- src/_frontend/warpClient.md | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_frontend/dataModel.md b/src/_frontend/dataModel.md index 3e82324..b7cd5a2 100644 --- a/src/_frontend/dataModel.md +++ b/src/_frontend/dataModel.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} ## Overview diff --git a/src/_frontend/downlinks.md b/src/_frontend/downlinks.md index 12e647e..72883d7 100644 --- a/src/_frontend/downlinks.md +++ b/src/_frontend/downlinks.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} A Downlink provides a virtual bidirectional stream between the client and a lane of a remote Web Agent. WARP clients transparently multiplex all links to [**Web Agents**]({% link _backend/web-agents.md %}) on a given host over a single WebSocket connection. diff --git a/src/_frontend/eventDownlink.md b/src/_frontend/eventDownlink.md index 625823a..4eb9e1d 100644 --- a/src/_frontend/eventDownlink.md +++ b/src/_frontend/eventDownlink.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} `EventDownlink` is not so much a subtype of `Downlink` as it is the base type of all downlinks. While not literally a superclass of `ValueDownlink`, `MapDownlink`, and `ListDownlink`, `EventDownlink` inherits from the same prototype as the others but contains no additional frills. For example, `MapDownlink` and `ListDownlink` support registering different callbacks for observing when a key-value pair has been added versus when one has been removed; and `ValueDownlink` has the `didSet` callback for when its value has been updated. `EventDownlink`, on the other hand, offers no specialized handling of WARP messages with respect to the type of Web Agent lane it is connected to. It provides a raw view of a WARP link, passing all received messages to a single `onEvent` callback. diff --git a/src/_frontend/form.md b/src/_frontend/form.md index 62baa60..b2e50ea 100644 --- a/src/_frontend/form.md +++ b/src/_frontend/form.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} A Form defines a conversion between a structural type, and some nominal JavaScript type. The `mold` method converts a nominal JavaScript type to an Item. And the `cast` method converts an Item to a nominal JavaScript type, if possible. diff --git a/src/_frontend/gettingStarted.md b/src/_frontend/gettingStarted.md index 6480445..54f53ad 100644 --- a/src/_frontend/gettingStarted.md +++ b/src/_frontend/gettingStarted.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} Swim is a full stack **streaming application platform** for building stateful services, streaming APIs, and real-time UIs. Streaming applications push differential state changes through the full application stack, eliminating the need for polling and streaming only what each client chooses to observe. Real-time UIs render live views of distributed application state. diff --git a/src/_frontend/listDownlink.md b/src/_frontend/listDownlink.md index 3234c04..3d74de3 100644 --- a/src/_frontend/listDownlink.md +++ b/src/_frontend/listDownlink.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} A ListDownlink synchronizes a shared real-time list with a remote list lane. In addition to the standard Downlink callbacks, ListDownlink supports registering `willUpdate`, `didUpdate`, `willMove`, `didMove`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked list state — whether remote or local. diff --git a/src/_frontend/mapDownlink.md b/src/_frontend/mapDownlink.md index 11095e8..0479215 100644 --- a/src/_frontend/mapDownlink.md +++ b/src/_frontend/mapDownlink.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} A MapDownlink synchronizes a shared real-time key-value map with a remote [**map lane**]({% link _backend/map-lanes.md %}). MapDownlinks are also the downlink best suited for use with [**join value lanes**]. Much like a map lane, join value lanes are key-value maps where each value is itself a link to another value lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md index 69b5e3c..7e08008 100644 --- a/src/_frontend/structures.md +++ b/src/_frontend/structures.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} ## Item diff --git a/src/_frontend/valueDownlink.md b/src/_frontend/valueDownlink.md index 5607379..b209fae 100644 --- a/src/_frontend/valueDownlink.md +++ b/src/_frontend/valueDownlink.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} A ValueDownlink synchronizes a shared real-time value with a remote value lane. In addition to the standard Downlink callbacks, ValueDownlink supports registering `willSet` and `didSet` callbacks to observe all changes to downlinked state — whether remote or local. diff --git a/src/_frontend/warpClient.md b/src/_frontend/warpClient.md index 4abe0e4..82e2525 100644 --- a/src/_frontend/warpClient.md +++ b/src/_frontend/warpClient.md @@ -7,7 +7,7 @@ layout: documentation redirect_from: --- -_This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior._ +{% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} **WarpClient** is the class which behaves as the primary mechanism for handling connection management and link routing. WARP clients transparently multiplex all links to [**Web Agents**]({% link _backend/web-agents.md %}) on a given host over a single WebSocket connection, and automatically manage the network connection to each host, including reconnection and resynchronization after a network failure. Key lifecycle events may also be observed through the registration of callbacks. From 3a1bbfa6931e387960728493ab6a0f1acd1b2aaa Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 4 Mar 2024 13:27:38 -0800 Subject: [PATCH 07/17] Update src/_frontend/downlinks.md Co-authored-by: Thomas Klapwijk --- src/_frontend/downlinks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/downlinks.md b/src/_frontend/downlinks.md index 72883d7..216a66d 100644 --- a/src/_frontend/downlinks.md +++ b/src/_frontend/downlinks.md @@ -91,7 +91,7 @@ const downlink = client downlink.close(); ``` -Closing a downlink does not necessarily close the underlying WARP link. The WARP client will keep a link open so long as at least one downlink to a given node and lane URI remains open. This prevents application components from stepping on each other's toes when they link to the same lanes of the same Web Agents. This can happen, for example, when a UI has a summary view and a detail view both display information derived from the same remote lane. The WARP link should not be closed when a detail view is hidden, if state updates are still required by the summary view. Events should also not be sent twice: once for the summary view, and once for the detail view. Neither the summary view nor the detail view should have to know about each other. And no global event dispatcher should be required, which could introduce consistency problems. WARP clients efficiently, and transparently handle all of these cases on behalf of all downlinks. +Closing a downlink does not necessarily close the underlying WARP link. The WARP client will keep a link open so long as at least one downlink to a given node and lane URI remains open. This prevents application components from stepping on each other's toes when they link to the same lanes of the same Web Agents. This can happen, for example, when a UI has a summary view and a detail view both display information derived from the same remote lane. The WARP link should not be closed when a detail view is hidden, if state updates are still required by the summary view. Events should also not be sent twice: once for the summary view, and once for the detail view. Neither the summary view nor the detail view should have to know about each other. And no global event dispatcher should be required, which could introduce consistency problems. WARP clients efficiently, and transparently, handle all of these cases on behalf of all downlinks. ## Downlink State and Lifecycle Callbacks From 4d2fb873dec844a71c588c1cb90c8cd47c10f380 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 4 Mar 2024 13:29:00 -0800 Subject: [PATCH 08/17] Update src/_frontend/form.md Co-authored-by: Thomas Klapwijk --- src/_frontend/form.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/form.md b/src/_frontend/form.md index b2e50ea..0b5e81e 100644 --- a/src/_frontend/form.md +++ b/src/_frontend/form.md @@ -11,7 +11,7 @@ redirect_from: A Form defines a conversion between a structural type, and some nominal JavaScript type. The `mold` method converts a nominal JavaScript type to an Item. And the `cast` method converts an Item to a nominal JavaScript type, if possible. -Here is an example of a custom Form which defines how the conversion of an entity representing a stock might look. Custom Forms should extend `Form`, which can be imported from `@swim/structure`. This library should automatically be present in your bundle as a dependency of `@swim/client`. +Here is an example of a custom Form which defines how the conversion of an entity representing how a stock might look. Custom Forms should extend `Form`, which can be imported from `@swim/structure`. This library should automatically be present in your bundle as a dependency of `@swim/client`. ```typescript import { Form, Item } from "@swim/structure"; From 05d3d8bc86e508ddd3ea8df48b8bcfd76c022681 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 4 Mar 2024 13:30:08 -0800 Subject: [PATCH 09/17] Update src/_frontend/structures.md Co-authored-by: Thomas Klapwijk --- src/_frontend/structures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md index 7e08008..ce19e7c 100644 --- a/src/_frontend/structures.md +++ b/src/_frontend/structures.md @@ -37,7 +37,7 @@ There are two unit types: `Extant`, and `Absent`. Extant represents a thing that ### Composite Value Type -A `Record` is a simple container of Item members, and is the only composite structure type. A Record containing only Field members is analogous to a JSON object—though unlike JSON, its keys are not restricted to strings. A Record containing only Value members is similar to a JSON array. A Record with a leading Attr bears resemblance to an XML element. And a Record with a mixture of Field and Value members acts like a partially keyed list. +A `Record` is a simple container of Item members, and is the only composite structure type. A Record containing only Field members is analogous to a JSON object — though, unlike JSON, its keys are not restricted to strings. A Record containing only Value members is similar to a JSON array. A Record with a leading Attr bears resemblance to an XML element. And a Record with a mixture of Field and Value members acts like a partially keyed list. ## Item Reference From 8e10208b7db2ae814f473684df9037a418049657 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 4 Mar 2024 13:41:34 -0800 Subject: [PATCH 10/17] Update src/_frontend/valueDownlink.md Co-authored-by: Thomas Klapwijk --- src/_frontend/valueDownlink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_frontend/valueDownlink.md b/src/_frontend/valueDownlink.md index b209fae..17b47f9 100644 --- a/src/_frontend/valueDownlink.md +++ b/src/_frontend/valueDownlink.md @@ -1,7 +1,7 @@ --- title: Value Downlink short-title: Value Downlink -description: "A WARP connection which synchronizes a shares real-time, scalar value with a remote value lane" +description: "A WARP connection which synchronizes a shared real-time, scalar value with a remote value lane" group: Connections layout: documentation redirect_from: From 08fe0b7407ae2bf98ced2aef902d334829fc5003 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 4 Mar 2024 13:42:30 -0800 Subject: [PATCH 11/17] Update src/_frontend/whatIsWarp.md Co-authored-by: Thomas Klapwijk --- src/_frontend/whatIsWarp.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_frontend/whatIsWarp.md b/src/_frontend/whatIsWarp.md index 4611613..2a9b6a4 100644 --- a/src/_frontend/whatIsWarp.md +++ b/src/_frontend/whatIsWarp.md @@ -15,7 +15,8 @@ Things that continuously evolve — like smart cities, autonomous control system In recent years, the WebSocket protocol has emerged, enabling us to open streaming connections to URIs. This is great. But it only solves part of the problem. Applications need to open a new WebSocket connection for every URI to which they want to connect. This limitation prevents WebSockets alone from serving as a streaming layer of the World Wide Web. But that's OK; that's not WebSocket's role. -## WARP +## WARP (Web Agent Remote Protocol) + WARP is a protocol for multiplexing bidirectional streams between large numbers of URIs over a single WebSocket connection. Multiplexed streams within a WARP connection are called **links**. Just as the World Wide Web has hypertext links between Web Pages, WARP enables actively streaming links between [**Web Agents**]({% link _backend/web-agents.md %}) and a browser client or another Web Agent. WARP is like pub-sub without the broker, enabling every state of a Web API to be streamed, without interference from billions of queues. Whether you have a UI trying to display the most up-to-date state, or a Web Agent aggregating real-time data from multiple sources into something more meaningful, lighning fast two-way communication through multiple data streams is made possible over a single WebSocket connection with WARP. From e2117bfadf1b1cb1c0b56134eb716dc35c6fb9b2 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Tue, 5 Mar 2024 18:05:23 -0800 Subject: [PATCH 12/17] Improve docs on WarpClient downlink callback methods --- src/_frontend/downlinks.md | 130 +++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 12 deletions(-) diff --git a/src/_frontend/downlinks.md b/src/_frontend/downlinks.md index 72883d7..bf4cb70 100644 --- a/src/_frontend/downlinks.md +++ b/src/_frontend/downlinks.md @@ -11,7 +11,7 @@ redirect_from: A Downlink provides a virtual bidirectional stream between the client and a lane of a remote Web Agent. WARP clients transparently multiplex all links to [**Web Agents**]({% link _backend/web-agents.md %}) on a given host over a single WebSocket connection. -Downlinks come in several flavors, depending on the WARP subprotocol to which they conform. A [**ValueDownlink**]({% link _frontend/valueDownlink.md %}) synchronizes a structured value with a remote value lane. A [**MapDownlink**]({% link _frontend/mapDownlink.md %}) implements the WARP map subprotocol to synchronize key-value state with a remote map lane. A [**ListDownlink**]({% link _frontend/listDownlink.md %}) implements the WARP list subprotocol to to synchronize sequential list state with a remote list lane. And an [**EventDownlink**]({% link _frontend/eventDownlink.md %}) observes raw WARP events, and can be used to observe lanes of any kind. +Downlinks come in several flavors, depending on the WARP subprotocol to which they conform. A [**ValueDownlink**]({% link _frontend/valueDownlink.md %}) synchronizes a value with a remote value lane. A [**MapDownlink**]({% link _frontend/mapDownlink.md %}) implements the WARP map subprotocol to synchronize key-value state with a remote map lane. A [**ListDownlink**]({% link _frontend/listDownlink.md %}) implements the WARP list subprotocol to to synchronize sequential list state with a remote list lane. And an [**EventDownlink**]({% link _frontend/eventDownlink.md %}) observes raw WARP events, and can be used to observe lanes of any kind. This article will focus on the properties and methods which all types of downlinks have in common. Later articles on specific types of downlinks will go into detail on what is unique to each of them. @@ -56,11 +56,9 @@ const lightingDownlink = client In addition to `hostUri`, `nodeUri`, and `laneUri`, there are a few other options available for customizing a downlink's behavior. -The `relinks` option determines whether or not a downlink should be automatically reopened after a network failure; it defaults to true. +The `relinks` option determines whether or not a downlink should be automatically reopened after a network failure; it defaults to `true`. -The `syncs` parameter determines whether or not a downlink should synchronize with the remote lane when opened; it defaults to true for stateful lanes. When set to true, a newly opened downlink will be sent the lane's current state and will be subscribed to all future updates. When set to false, a downlink will receive future updates but will not be provided the lane's state at the time of opening. - - +The `syncs` parameter determines whether or not a downlink should synchronize with the remote lane when opened; it defaults to `true` for stateful lanes. When set to `true`, a newly opened downlink will be sent the lane's current state and will be subscribed to all future updates. When set to `false`, a downlink will receive future updates but will not be provided the lane's state at the time of opening. ```javascript const client = new WarpClient(); @@ -95,16 +93,124 @@ Closing a downlink does not necessarily close the underlying WARP link. The WARP ## Downlink State and Lifecycle Callbacks -A number of methods are made available for retrieving key pieces of a downlink's state. Optional callbacks may also be registered for reacting to changes in these states or other key lifecycle events. Callbacks may be included in the options object passed when creating a downlink, or set individually after a downlink has been initialized. +A number of methods are made available for retrieving key pieces of a downlink's state. Optional callbacks may also be registered for reacting to changes in these states or other key lifecycle events. Callbacks may be included in the options object passed when creating a downlink or set individually after a downlink has been initialized. + +### Connections + +The `connected` method returns `true` if the underlying connection to the remote host is currently open. `didConnect` registers an observer callback that gets invoked whenever a successful connection is made. Likewise, `didDisconnect` and `didClose` register observer callbacks which gets invoked when a connection has ended. The difference between the two is that `didDisconnect` is triggered when the host severs the connection and `didClose` is triggered when the client initiates the disconnection. + +Here is an example of a downlink being opened with some registered callbacks for listening to connection status. + +```javascript +const downlink = client.current.downlink({ + hostUri: "warp://example.com", + nodeUri: "hotel/room/123", + laneUri: "status", + didConnect: () => { console.log("didConnect"); }, + didDisconnect: () => { console.log("didDisconnect"); }, + didClose: () => { console.log("didClose"); }, +}) +.open(); + +setTimeout(() => { downlink.close(); }, 1000); + +/* Output: + didConnect + didClose */ +``` + +Here is what the same example would look like if, instead of calling `downlink.close()`, the client began to experience network issues or if the host suddenly went offline. + +```javascript +const downlink = client.current.downlink({ + hostUri: "warp://example.com", + nodeUri: "hotel/room/123", + laneUri: "status", + didConnect: () => { console.log("didConnect"); }, + didDisconnect: () => { console.log("didDisconnect"); }, + didClose: () => { console.log("didClose"); }, +}) +.open(); + +/* (some network issues) */ + +/* Output: + didConnect + didDisconnect */ +``` + +### Linking and Syncing + +The `linked` method returns `true` if the logical WARP link is currently open. Changes to the link's state may be observed by registering callbacks with `willLink` or `didLink`. + +The `synced` method returns `true` if the WARP link is currently synchronized with the state of the remote lane. Users may observe synchronization using the `willSync` and `didSync` methods. + +`willLink` and `willSync` are both preemptive observers; they will be called regardless of the success or failure of the subsequent linking or syncing operations. Furthermore, because of the WARP messages the client sends to open a downlink, only one of either `willLink` or `willSync` will be invoked when a downlink is opened. Which observer gets called depends on the value of [`syncs`](/frontend/downlinks#other-options). When the value of `syncs` is `false`, a WARP message with the "@link" tag is sent to the host; when `syncs` is `true`, a message with the "@sync" tag is sent instead. When the host receives a "@sync" WARP message it understands that it must perform all of the logic triggered by a "@link" message and, additionally, sync state between it and the new client. + +Take this example of opening a simple `ValueDownlink`. Notice that `syncs` is set to `false` so we see "willLink" logged to output. + +```javascript +const downlink = client.current.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "hotel/room/123", + laneUri: "status", + syncs: true, + willLink: () => { console.log("willLink"); }, + didLink: () => { console.log("didLink"); }, + willSync: () => { console.log("willSync"); }, + didSync: () => { console.log("didSync"); }, + didSet: () => { console.log("didSet"); }, + didClose: () => { console.log("didClose"); }, +}) +.open(); + +setTimeout(() => { downlink.close(); }, 1000); + +/* Output: + willLink + didLink + didClose */ +``` + +When `syncs` is set to `true`, "didSync" appears and "willSyncs" replaces "willLink". + +```javascript +const downlink = client.current.downlinkValue({ + hostUri: "warp://example.com", + nodeUri: "hotel/room/123", + laneUri: "status", + syncs: true, + willLink: () => { console.log("willLink"); }, + didLink: () => { console.log("didLink"); }, + willSync: () => { console.log("willSync"); }, + didSync: () => { console.log("didSync"); }, + didSet: () => { console.log("didSet"); }, + didClose: () => { console.log("didClose"); }, +}) +.open(); + +setTimeout(() => { downlink.close(); }, 1000); + +/* Output: + willSync + didLink + didSet + didSync + didClose */ +``` + +Notice the callbacks registered for `didLink` and `didSync` both always get called (assuming the link opened successfulfully). This is because the host sends back a separate message after each of these events occur. In the second example, we also see "didSet" appear in the output. `didSet` is called each time a `ValueDownlink` receives an update to the value shared between the client and host. When `syncs` is set to `true`, we will always receive at least the initial value, assuring `didSet` gets called at least once. When `syncs` is set to `false`, this is not guaranteed. We'll cover all of this in more detail later in the [**valueDownlinks**]({% link _frontend/valueDownlink.md %}) article. + +The `opened` method returns `true` if the downlink has been opened. This is not necessarily always the same value as `linked`. Providing a downlink with an invalid `hostUri`, for example, could result in `opened` returning `true` and `linked` returning `false`. -The `connected` method returns true if the underlying connection to the remote host is currently open. `didConnect` registers an observer callback that gets invoked whenever a successful connection is made. Likewise, `didDisconnect` registers an observer callback which gets invoked when a connection has ended. +### Authentication -The `linked` method returns true if the logical WARP link is currently open. Changes to the link's state may be observed by registering callbacks with `willLink`, `didLink`, `willUnlink`, or `didUnlink`. +The `authenticated` method returns `true` if the underlying connection to the remote host is currently authenticated. -The `synced` method returns true if the WARP link is currently synchronized with the state of the remote lane. Users may observe synchronization using the `willSync` and `didSync` methods. +### Bidirectional Communication -The `opened` method returns true if the downlink has been opened. This is not necessarily always the same value as `linked`. Providing a downlink with an invalid hostUri, for example, could result in `opened` returning true and `linked` returning false. `didClose` gets invoked when `close` is called on a downlink. +And finally, all downlinks support registering `onEvent` and `onCommand` callbacks. -The `authenticated` method returns true if the underlying connection to the remote host is currently authenticated. +The `onCommand` method accepts a callback used for observing outgoing command messages. -And finally, all downlinks support registering `onEvent` and `onCommand` callbacks. The `onCommand` method accepts a callback used for observing outgoing command messages. The `onEvent` method registers a callback for observing all incoming events. `onEvent` is the rawest form of handling WARP messages. In most cases, users will be better off handling incoming updates with specialized observer callbacks defined on downlink subtypes because they provide better context about the type of message being received, allowing us to handle them more appropriately. More on this in later sections. +The `onEvent` method registers a callback for observing all incoming events. `onEvent` is the rawest form of handling WARP messages. In most cases, users will be better off handling incoming updates with specialized observer callbacks defined on downlink subtypes. Specialized downlink subtypes provide better context about the type of message being received, allowing us to handle them more appropriately. More on this in later sections. From 96f38138550ffc9ea6a03bb15a8c0260526f632e Mon Sep 17 00:00:00 2001 From: Cameron James Date: Tue, 5 Mar 2024 18:05:47 -0800 Subject: [PATCH 13/17] Minor doc improvements --- src/_frontend/gettingStarted.md | 18 --- src/_frontend/structures.md | 198 ++++++++++++++++---------------- 2 files changed, 102 insertions(+), 114 deletions(-) diff --git a/src/_frontend/gettingStarted.md b/src/_frontend/gettingStarted.md index 54f53ad..c706b48 100644 --- a/src/_frontend/gettingStarted.md +++ b/src/_frontend/gettingStarted.md @@ -43,13 +43,6 @@ import { WarpClient } from "@swim/client"; import { Value } from "@swim/structure"; ``` - - **Browser / CDN** The swim.js script bundles exports from `@swim/client`, its Swim dependencies, and a number of additional Swim libraries useful for building real time UIs, including tools for making charts and maps. When loaded by a web browser, the swim.js script adds all library exports to the global `swim` namespace. @@ -63,17 +56,6 @@ const client = new swim.WarpClient(); Typescript definition files are provided in the libraries. All downlink variants support the ability to provide type information for incoming and outgoing WARP messages. The exception to this is [EventDownlink]({% link _frontend/eventDownlink.md %}), which should rarely be used. See our article on [**Forms**]({% link _frontend/form.md %}) for more details and an example of how to provide downlinks with type information. - - ## Quick Start diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md index 7e08008..6f03e0a 100644 --- a/src/_frontend/structures.md +++ b/src/_frontend/structures.md @@ -11,9 +11,9 @@ redirect_from: ## Item -At the center of @swim/structure is the `Item` class, which defines an algebraic data type for representing and manipulating structured data. Item provides many methods for operating on structured values, most of which are closed over the Item type, meaning they always return other instances of Item. This closure of operations over the Item type makes it safe and expressive to traverse, transform, and convert arbitrary data structures, without excessive conditional logic to type check and validate structures obtained from external sources. +At the center of @swim/structure is the `Item` class, which defines an algebraic data type for representing and manipulating structured data. `Item` provides many methods for operating on structured values, most of which are closed over the `Item` type, meaning they always return other instances of `Item`. This closure of operations over the `Item` type makes it safe and expressive to traverse, transform, and convert arbitrary data structures, without excessive conditional logic to type check and validate structures obtained from external sources. -Every `Item` is either a Field or a Value. Every Field is either an Attr or a Slot. And every Value is either a Record, Data, Text, Num, Bool, Extant, or Absent. Think of Item as analogous to the set of all JSON values, with the inclusion of object fields as first class elements. +Every `Item` is either a `Field` or a `Value`. Every `Field` is either an `Attr` or a `Slot`. And every `Value` is either a `Record`, `Data`, `Text`, `Num`, `Bool`, `Extant`, or `Absent`. Think of `Item` as analogous to the set of all JSON values, with the inclusion of object fields as first class elements.
Swim data structures; Item, Field, and Value @@ -21,11 +21,11 @@ Every `Item` is either a Field or a Value. Every Field is either an Attr or a Sl ## Field -A `Field` represents a key-value pair, where both the key and value are of type Value. An `Attr` is a discriminated kind of Field whose key is always of type Text. Every Field that is not explicitly an Attr is a `Slot`. Think of a Slot as a field of a JSON object, or as an attribute of an XML tag. Think of an Attr like an XML tag, where the key of the Attr is the tag name, and the value of the Attr is a Record containing the element's attributes. +A `Field` represents a key-value pair, where both the key and value are of type `Value`. An `Attr` is a discriminated kind of `Field` whose key is always of type `Text`. Every `Field` that is not explicitly an `Attr` is a `Slot`. Think of a `Slot` as a field of a JSON object, or as an attribute of an XML tag. Think of an `Attr` like an XML tag, where the key of the `Attr` is the tag name, and the value of the `Attr` is a `Record` containing the element's attributes. ## Value -Every Item that is not a Field is a `Value`. A Value can either be one of four primitive value types: Data, Text, Num, or Bool; one of two unit types: Extant, or Absent; or the composite type: Record. Think of a Value as representing an arbitrary data structure. +Every `Item` that is not a `Field` is a `Value`. A `Value` can either be one of four primitive value types: `Data`, `Text`, `Num`, or `Bool`; one of two unit types: `Extant`, or `Absent`; or the composite type: `Record`. Think of a `Value` as representing an arbitrary data structure. ### Primitive Value Types @@ -33,157 +33,163 @@ A `Data` object represents opaque binary data; it wraps a JavaScript Uint8Array. ### Unit Value Types -There are two unit types: `Extant`, and `Absent`. Extant represents a thing that exists, but has no value; sort of like JavaScript's null value, but a valid object on which you can invoke methods. Absent represents something that does not exist; similar to JavaScript's undefined value, but a valid instance of Item. +There are two unit types: `Extant`, and `Absent`. `Extant` represents a thing that exists, but has no value; sort of like JavaScript's null value, but a valid object on which you can invoke methods. `Absent` represents something that does not exist; similar to JavaScript's undefined value, but a valid instance of Item. ### Composite Value Type -A `Record` is a simple container of Item members, and is the only composite structure type. A Record containing only Field members is analogous to a JSON object—though unlike JSON, its keys are not restricted to strings. A Record containing only Value members is similar to a JSON array. A Record with a leading Attr bears resemblance to an XML element. And a Record with a mixture of Field and Value members acts like a partially keyed list. +A `Record` is a simple container of `Item` members, and is the only composite structure type. A `Record` containing only `Field` members is analogous to a JSON object—though unlike JSON, its keys are not restricted to strings. A `Record` containing only `Value` members is similar to a JSON array. A `Record` with a leading `Attr` bears resemblance to an XML element. And a `Record` with a mixture of `Field` and `Value` members acts like a partially keyed list. ## Item Reference -Since everything is an Item, each of these methods are available on every kind of structure. +Since everything is an `Item`, each of these methods are available on every kind of structure. -`isDefined(): boolean;` +`isDefined(): boolean;` -Returns _true_ if this _Item_ is not _Absent_. +Returns `true` if this `Item` is not `Absent`. -`isDistinct(): boolean;` +`isDistinct(): boolean;` -Returns _true_ if this _Item_ is neither _Extant_ nor _Absent_. +Returns `true` if this `Item` is neither `Extant` nor `Absent`. -`isDefinite(): boolean;` +`isDefinite(): boolean;` -Returns _true_ if this _Item_ is not one of: an empty _Record_, _False_, _Extant_, or _Absent_. +Returns `true` if this `Item` is not one of: an empty `Record`, `False`, `Extant`, or `Absent`. -`isConstant(): boolean;` +`isConstant(): boolean;` -Returns _true_ if this _Item_ always _evaluates_ to the same _Item_. +Returns `true` if this `Item` always `evaluates` to the same `Item`. -`readonly key: Value;` +`readonly key: Value;` -Returns the key component of this _Item_, if this _Item_ is a Field; otherwise returns Absent if this _Item_ is a _Value_. +Returns the key component of this `Item`, if this `Item` is a Field; otherwise returns Absent if this `Item` is a `Value`. -`toValue(): Value;` +`toValue(): Value;` -Returns the value component of this _Item_, if this _Item_ is a Field; otherwise returns _this_ if this _Item_ is a _Value_. +Returns the value component of this `Item`, if this `Item` is a Field; otherwise returns `this` if this `Item` is a `Value`. -`readonly tag: string | undefined;` +`readonly tag: string | undefined;` -Returns the _key_ string of the first member of this _Item_, if this _Item_ +Returns the `key` string of the first member of this `Item`, if this `Item` is a Record, and its first member is an Attr; otherwise returns -_undefined_ if this _Item_ is not a _Record_, or if this _Item_ is a _Record_ whose first member is not an _Attr_. -Used to concisely get the name of the discriminating attribute of a structure. The _tag_ can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag. +`undefined` if this `Item` is not a `Record`, or if this `Item` is a `Record` whose first member is not an `Attr`. +Used to concisely get the name of the discriminating attribute of a structure. The `tag` can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag. -`readonly target: Value;` +`readonly target: Value;` -Returns the _flattened_ members of this _Item_ after all -attributes have been removed, if this _Item_ is a Record; otherwise returns _this_ if this _Item_ is a _non-Record_ _Value_, or returns the value component if this _Item_ is a _Field_. -Used to concisely get the scalar value of an attributed structure. An attributed structure is a _Record_ with one or more attributes that modify one or more other members. +If this `Item` is a Record, returns the `flattened` members of this `Item` after all attributes have been removed; +otherwise returns `this` if this `Item` is a `non-Record` `Value`, or returns the value component if this `Item` is a `Field`. +Used to concisely get the scalar value of an attributed structure. An attributed structure is a `Record` with one or more attributes that modify one or more other members. -`flattened(): Value;` +`flattened(): Value;` -Returns the sole member of this _Item_, if this _Item_ is a Record -with exactly one member, and its member is a _Value_; returns Extant -if this _Item_ is an empty _Record_; returns Absent if this _Item_ is -a _Field_; otherwise returns _this_ if this _Item_ is a _Record_ with more -than one member, or if this _Item_ is a _non-Record_ _Value_. -Used to convert a unary _Record_ into its member _Value_. Facilitates -writing code that treats a unary _Record_ equivalently to a bare _Value_. +Returns the sole member of this `Item`, if this `Item` is a Record +with exactly one member, and its member is a `Value`; returns Extant +if this `Item` is an empty `Record`; returns Absent if this `Item` is +a `Field`; otherwise returns `this` if this `Item` is a `Record` with more +than one member, or if this `Item` is a `non-Record` `Value`. +Used to convert a unary `Record` into its member `Value`. Facilitates +writing code that treats a unary `Record` equivalently to a bare `Value`. -`unflattened(): Record;` +`unflattened(): Record;` -Returns _this_ if this _Item_ is a Record; returns a _Record_ -containing just this _Item_, if this _Item_ is _distinct_; otherwise returns an empty _Record_ if this _Item_ is Extant or Absent. Facilitates writing code that treats a bare _Value_ equivalently to a unary _Record_. +Returns `this` if this `Item` is a Record; returns a `Record` +containing just this `Item`, if this `Item` is `distinct`; otherwise returns an empty `Record` if this `Item` is Extant or Absent. Facilitates writing code that treats a bare `Value` equivalently to a unary `Record`. -`header(tag: string): Value;` +`header(tag: string): Value;` -Returns the value of the first member of this _Item_, if this _Item_ is a -Record, and its first member is an Attr whose _key_ string is equal to _tag_; otherwise returns Absent if this _Item_ is not a _Record_, or if this _Item_ is a _Record_ whose first member is not an _Attr_, or if this _Item_ is a _Record_ whose first member is an _Attr_ whose _key_ does not equal the _tag_. Used to conditionally get the value of the head _Attr_ of a structure, if and only if the key string of the head _Attr_ is equal to the _tag_. Can be used to check if a structure might conform to a nominal type named _tag_, while simultaneously getting the value of the _tag_ attribute. +Returns the value of the first member of this `Item`, if this `Item` is a +Record, and its first member is an Attr whose `key` string is equal to `tag`; otherwise returns Absent if this `Item` is not a `Record`, or if this `Item` is a `Record` whose first member is not an `Attr`, or if this `Item` is a `Record` whose first member is an `Attr` whose `key` does not equal the `tag`. Used to conditionally get the value of the head `Attr` of a structure, if and only if the key string of the head `Attr` is equal to the `tag`. Can be used to check if a structure might conform to a nominal type named `tag`, while simultaneously getting the value of the `tag` attribute. -`headers(tag: string): Record | undefined;` +`headers(tag: string): Record | undefined;` -Returns the _unflattened_ _header_ of -this _Item_, if this _Item_ is a Record, and its first member is an Attr whose _key_ string is equal to _tag_; otherwise returns _undefined_. The _headers_ of the _tag_ attribute of a structure are like the attributes of an XML element tag; through unlike an XML element, _tag_ attribute headers are not limited to string keys and values. +Returns the `unflattened` `header` of +this `Item`, if this `Item` is a Record, and its first member is an Attr whose `key` string is equal to `tag`; otherwise returns `undefined`. The `headers` of the `tag` attribute of a structure are like the attributes of an XML element tag; through unlike an XML element, `tag` attribute headers are not limited to string keys and values. -`head(): Item;` +`head(): Item;` -Returns the first member of this _Item_, if this _Item_ is a non-empty +Returns the first member of this `Item`, if this `Item` is a non-empty Record; otherwise returns Absent. -`tail(): Record;` +`tail(): Record;` -Returns a view of all but the first member of this _Item_, if this _Item_ -is a non-empty _Record_; otherwise returns an empty _Record_ if this _Item_ is not a _Record_, or if this _Item_ is itself an empty _Record_. +Returns a view of all but the first member of this `Item`, if this `Item` +is a non-empty `Record`; otherwise returns an empty `Record` if this `Item` is not a `Record`, or if this `Item` is itself an empty `Record`. -`body(): Value;` +`body(): Value;` -Returns the _flattened_ _tail_ of this -_Item_. Used to recursively deconstruct a structure, terminating with its last _Value_, rather than a unary _Record_ containing its last value, if the structure ends with a _Value_ member. +Returns the `flattened` `tail` of this +`Item`. Used to recursively deconstruct a structure, terminating with its last `Value`, rather than a unary `Record` containing its last value, if the structure ends with a `Value` member. -`readonly length: number;` +`readonly length: number;` -Returns the number of members contained in this _Item_, if this _Item_ is -a Record; otherwise returns _0_ if this _Item_ is not a _Record_. +Returns the number of members contained in this `Item`, if this `Item` is +a Record; otherwise returns `0` if this `Item` is not a `Record`. -`has(key: ValueLike): boolean;` +`has(key: ValueLike): boolean;` -Returns _true_ if this _Item_ is a Record that has a Field member -with a key that is equal to the given _key_; otherwise returns _false_ if this _Item_ is not a _Record_, or if this _Item_ is a _Record_, but has no _Field_ member with a key equal to the given _key_. +Returns `true` if this `Item` is a Record that has a Field member +with a key that is equal to the given `key`; otherwise returns `false` if this `Item` is not a `Record`, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given `key`. -`get(key: ValueLike): Value;` +`get(key: ValueLike): Value;` -Returns the value of the last Field member of this _Item_ whose key -is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Field_ member with a key equal to the given _key_. +Returns the value of the last Field member of this `Item` whose key +is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given `key`. -`getAttr(key: TextLike): Value;` +`getAttr(key: TextLike): Value;` -Returns the value of the last Attr member of this _Item_ whose key -is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Attr_ member with a key equal to the given _key_. +Returns the value of the last Attr member of this `Item` whose key +is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Attr` member with a key equal to the given `key`. -`getSlot(key: ValueLike): Value;` +`getSlot(key: ValueLike): Value;` -Returns the value of the last Slot member of this _Item_ whose key -is equal to the given _key_; returns Absent if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Slot_ member with a key equal to the given _key_. +Returns the value of the last Slot member of this `Item` whose key +is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Slot` member with a key equal to the given `key`. -`getField(key: ValueLike): Field | undefined;` +`getField(key: ValueLike): Field | undefined;` -Returns the last Field member of this _Item_ whose key is equal to the -given _key_; returns _undefined_ if this _Item_ is not a Record, or if this _Item_ is a _Record_, but has no _Field_ member with a _key_ equal to the given _key_. +Returns the last Field member of this `Item` whose key is equal to the +given `key`; returns `undefined` if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a `key` equal to the given `key`. -`getItem(index: NumLike): Item;` +`getItem(index: NumLike): Item;` -Returns the member of this _Item_ at the given _index_, if this _Item_ is -a Record, and the _index_ is greater than or equal to zero, and less than the _length_ of the _Record_; otherwise returns Absent if this _Item_ is not a _Record_, or if this _Item_ is a _Record_, but the _index_ is out of bounds. +Returns the member of this `Item` at the given `index`, if this `Item` is +a Record, and the `index` is greater than or equal to zero, and less than the `length` of the `Record`; otherwise returns Absent if this `Item` is not a `Record`, or if this `Item` is a `Record`, but the `index` is out of bounds. -`evaluate(interpreter: InterpreterLike): Item;` +`evaluate(interpreter: InterpreterLike): Item;` -Returns a new _Item_ with all nested expressions interpreted in lexical order and scope. +Returns a new `Item` with all nested expressions interpreted in lexical order and scope. -`stringValue(): string | undefined;` -Converts this _Item_ into a _string_ value, if possible; otherwise returns -_undefined_ if this _Item_ can't be converted into a _string_ value. +`stringValue(): string | undefined;` -`stringValue(orElse: T): string | T;` -Converts this _Item_ into a _string_ value, if possible; otherwise returns -_orElse_ if this _Item_ can't be converted into a _string_ value. +Converts this `Item` into a `string` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `string` value. -`numberValue(): number | undefined;` -Converts this _Item_ into a _number_ value, if possible; otherwise returns -_undefined_ if this _Item_ can't be converted into a _number_ value. +`stringValue(orElse: T): string | T;` -`numberValue(orElse: T): number | T;` -Converts this _Item_ into a _number_ value, if possible; otherwise returns -_orElse_ if this _Item_ can't be converted into a _number_ value. +Converts this `Item` into a `string` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `string` value. -`booleanValue(): boolean | undefined;` -Converts this _Item_ into a _boolean_ value, if possible; otherwise returns -_undefined_ if this _Item_ can't be converted into a _boolean_ value. +`numberValue(): number | undefined;` -`booleanValue(orElse: T): boolean | T;` -Converts this _Item_ into a _boolean_ value, if possible; otherwise returns -_orElse_ if this _Item_ can't be converted into a _boolean_ value. +Converts this `Item` into a `number` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `number` value. -`readonly typeOrder: number;` -Returns the heterogeneous sort order of this _Item_. Used to impose a +`numberValue(orElse: T): number | T;` + +Converts this `Item` into a `number` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `number` value. + +`booleanValue(): boolean | undefined;` + +Converts this `Item` into a `boolean` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `boolean` value. + +`booleanValue(orElse: T): boolean | T;` + +Converts this `Item` into a `boolean` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `boolean` value. + +`readonly typeOrder: number;` +Returns the heterogeneous sort order of this `Item`. Used to impose a total order on the set of all items. When comparing two items of -different types, the items order according to their _typeOrder_. +different types, the items order according to their `typeOrder`. From c8a4d994226f4f38406979c1d00f69c258babe2a Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 13 Mar 2024 10:29:22 -0700 Subject: [PATCH 14/17] Add code examples for Item APIs --- src/_frontend/structures.md | 214 ++++++++++++++++++++++++++++++------ 1 file changed, 179 insertions(+), 35 deletions(-) diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md index b4c909b..2fc2447 100644 --- a/src/_frontend/structures.md +++ b/src/_frontend/structures.md @@ -59,6 +59,93 @@ Returns `true` if this `Item` is not one of: an empty `Record`, `False`, `Extant Returns `true` if this `Item` always `evaluates` to the same `Item`. +`stringValue(): string | undefined;` + +Converts this `Item` into a `string` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `string` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("model")) // {value: 'Single Phase 4P Din Rail Energy Meter', hashValue: undefined, ...} +console.log(item.get("model").stringValue()) // "Single Phase 4P Din Rail Energy Meter" +console.log(item.get("currentReading").stringValue()) // "8432.7" +console.log(item.get("normalOperation").stringValue()) // "true" +``` + +`stringValue(orElse: T): string | T;` + +Converts this `Item` into a `string` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `string` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("foo")) // Absent {} +console.log(item.get("foo").stringValue()) // undefined +console.log(item.get("foo").stringValue("fallback")) // "fallback" +``` + +`numberValue(): number | undefined;` + +Converts this `Item` into a `number` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `number` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("currentReading")) // {value: 8432.7, hashValue: undefined, ...} +console.log(item.get("currentReading").numberValue()) // 8432.7 +console.log(item.get("model").numberValue()) // undefined +console.log(item.get("normalOperation").numberValue()) // undefined +``` + +`numberValue(orElse: T): number | T;` + +Converts this `Item` into a `number` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `number` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("foo")) // Absent {} +console.log(item.get("foo").numberValue()) // undefined +console.log(item.get("foo").numberValue(0)) // 0 +``` + +`booleanValue(): boolean | undefined;` + +Converts this `Item` into a `boolean` value, if possible; otherwise returns +`undefined` if this `Item` can't be converted into a `boolean` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("normalOperation")) // {value: true, hashValue: undefined, ...} +console.log(item.get("normalOperation").booleanValue()) // true +console.log(item.get("currentReading").booleanValue()) // true +console.log(item.get("model").booleanValue()) // undefined +``` + +`booleanValue(orElse: T): boolean | T;` + +Converts this `Item` into a `boolean` value, if possible; otherwise returns +`orElse` if this `Item` can't be converted into a `boolean` value. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("foo")) // Absent {} +console.log(item.get("foo").booleanValue()) // undefined +console.log(item.get("foo").booleanValue(false)) // false +``` + `readonly key: Value;` Returns the key component of this `Item`, if this `Item` is a Field; otherwise returns Absent if this `Item` is a `Value`. @@ -69,10 +156,30 @@ Returns the value component of this `Item`, if this `Item` is a Field; otherwise `readonly tag: string | undefined;` -Returns the `key` string of the first member of this `Item`, if this `Item` +Returns the key string of the first member of this `Item`, if this `Item` is a Record, and its first member is an Attr; otherwise returns `undefined` if this `Item` is not a `Record`, or if this `Item` is a `Record` whose first member is not an `Attr`. -Used to concisely get the name of the discriminating attribute of a structure. The `tag` can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag. +Used to concisely get the name of the discriminating attribute of a structure. The tag can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag. + +```typescript +/* + WARP message; update to value lane's synced value + @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} +*/ +console.log(item.tag); // undefined + +/* + WARP message; key updated or added to map-based lane + @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} +*/ +console.log(item.tag); // "update" + +/* + WARP message; key removed from map-based lane + @event(node:"/user/1234",lane:orders)@remove(key:"/order/456789") +*/ +console.log(item.tag); // "remove" +``` `readonly target: Value;` @@ -100,6 +207,15 @@ containing just this `Item`, if this `Item` is `distinct` Returns the value of the first member of this `Item`, if this `Item` is a Record, and its first member is an Attr whose `key` string is equal to `tag`; otherwise returns Absent if this `Item` is not a `Record`, or if this `Item` is a `Record` whose first member is not an `Attr`, or if this `Item` is a `Record` whose first member is an `Attr` whose `key` does not equal the `tag`. Used to conditionally get the value of the head `Attr` of a structure, if and only if the key string of the head `Attr` is equal to the `tag`. Can be used to check if a structure might conform to a nominal type named `tag`, while simultaneously getting the value of the `tag` attribute. +```typescript +// WARP message +// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} + +value.header('update'); // RecordMap {length: 1, fieldCount: 1, ...} +value.header('update').get('key'); // "/order/123456" +value.header('replace').get('key'); // Absent {} +``` + `headers(tag: string): Record | undefined;` Returns the `unflattened` `header` of @@ -125,69 +241,97 @@ Returns the `flattened` `tail` of this Returns the number of members contained in this `Item`, if this `Item` is a Record; otherwise returns `0` if this `Item` is not a `Record`. +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.length) // 4 +``` + `has(key: ValueLike): boolean;` Returns `true` if this `Item` is a Record that has a Field member -with a key that is equal to the given `key`; otherwise returns `false` if this `Item` is not a `Record`, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given `key`. +with a key that is equal to the given key; otherwise returns `false` if this `Item` is not a `Record`, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given key. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.has("model")) // true +console.log(item.has("currentReading")) // true +console.log(item.has("foo")) // false +``` `get(key: ValueLike): Value;` Returns the value of the last Field member of this `Item` whose key -is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given `key`. +is equal to the given key; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given key. + +```typescript +// WARP message +// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + +console.log(item.get("model")) // {value: 8432.7, hashValue: undefined, ...} +console.log(item.get("currentReading").numberValue()) // 8432.7 +console.log(item.get("model").stringValue()) // "Single Phase 4P Din Rail Energy Meter" +console.log(item.get("normalOperation").booleanValue()) // true +``` `getAttr(key: TextLike): Value;` Returns the value of the last Attr member of this `Item` whose key is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Attr` member with a key equal to the given `key`. +```typescript +// WARP message +// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} + +console.log(item.getAttr("update").get("key").stringValue()); // "/order/123456" +console.log(item.get("update").get("key").stringValue()); // "/order/123456" + +console.log(item.getAttr("totalPrice").numberValue()); // undefined +console.log(item.get("totalPrice").numberValue()); // 29.98 +``` + `getSlot(key: ValueLike): Value;` Returns the value of the last Slot member of this `Item` whose key is equal to the given `key`; returns Absent if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Slot` member with a key equal to the given `key`. +```typescript +// WARP message +// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} + +console.log(item.get("totalPrice").numberValue()); // 29.98 +console.log(item.getSlot("totalPrice").numberValue()); // 29.98 + +console.log(item.get("update").get("key").stringValue()); // "/order/123456" +console.log(item.getSlot("update").get("key").stringValue()); // "/order/123456" +``` + `getField(key: ValueLike): Field | undefined;` Returns the last Field member of this `Item` whose key is equal to the -given `key`; returns `undefined` if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a `key` equal to the given `key`. +given key; returns `undefined` if this `Item` is not a Record, or if this `Item` is a `Record`, but has no `Field` member with a key equal to the given key. `getItem(index: NumLike): Item;` Returns the member of this `Item` at the given `index`, if this `Item` is a Record, and the `index` is greater than or equal to zero, and less than the `length` of the `Record`; otherwise returns Absent if this `Item` is not a `Record`, or if this `Item` is a `Record`, but the `index` is out of bounds. -`evaluate(interpreter: InterpreterLike): Item;` - -Returns a new `Item` with all nested expressions interpreted in lexical order and scope. - -`stringValue(): string | undefined;` - -Converts this `Item` into a `string` value, if possible; otherwise returns -`undefined` if this `Item` can't be converted into a `string` value. - -`stringValue(orElse: T): string | T;` - -Converts this `Item` into a `string` value, if possible; otherwise returns -`orElse` if this `Item` can't be converted into a `string` value. - -`numberValue(): number | undefined;` - -Converts this `Item` into a `number` value, if possible; otherwise returns -`undefined` if this `Item` can't be converted into a `number` value. - -`numberValue(orElse: T): number | T;` - -Converts this `Item` into a `number` value, if possible; otherwise returns -`orElse` if this `Item` can't be converted into a `number` value. - -`booleanValue(): boolean | undefined;` +```typescript +// WARP message +// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} -Converts this `Item` into a `boolean` value, if possible; otherwise returns -`undefined` if this `Item` can't be converted into a `boolean` value. +console.log(item.getItem(0).key.stringValue()) // "update" +console.log(item.getItem(1).numberValue()) // 1710295106760 +console.log(item.getItem(2).numberValue()) // 8432.7 +console.log(item.getItem(99).stringValue()) // Absent {} +``` -`booleanValue(orElse: T): boolean | T;` +`evaluate(interpreter: InterpreterLike): Item;` -Converts this `Item` into a `boolean` value, if possible; otherwise returns -`orElse` if this `Item` can't be converted into a `boolean` value. +Returns a new `Item` with all nested expressions interpreted in lexical order and scope. `readonly typeOrder: number;` Returns the heterogeneous sort order of this `Item`. Used to impose a From e52560e18d786ba4fca72fec108d76fd7a9d9488 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 13 Mar 2024 10:29:38 -0700 Subject: [PATCH 15/17] Comment out listDownlink page for now --- _config.yml | 2 +- src/_frontend/listDownlink.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/_config.yml b/_config.yml index d23a2e3..b6abe05 100644 --- a/_config.yml +++ b/_config.yml @@ -166,7 +166,7 @@ collections: - downlinks.md - valueDownlink.md - mapDownlink.md - - listDownlink.md + # - listDownlink.md - eventDownlink.md # - property.md # - demandLanes.md diff --git a/src/_frontend/listDownlink.md b/src/_frontend/listDownlink.md index 3d74de3..02fa4f7 100644 --- a/src/_frontend/listDownlink.md +++ b/src/_frontend/listDownlink.md @@ -1,11 +1,11 @@ ---- + {% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} @@ -26,8 +26,8 @@ const listDownlink = client.downlinkList({ .open(); listDownlink.get(0); // get the first item in the list -listDownlink.set(0, { arrival: 1707956415650, nights: 1, guestName: "Jeff Lebowski" }); // locally and remotely update an item -listDownlink.push({ arrival: 1707748815650, nights: 2, guestName: "Walter Sobchak" }); // locally and remotely append an item +listDownlink.set(0, { arrival: "2024-04-01T15:00:00Z", nights: 1, guestName: "Jeff Lebowski" }); // locally and remotely update an item +listDownlink.push({ arrival: "2024-04-02T15:00:00Z", nights: 2, guestName: "Walter Sobchak" }); // locally and remotely append an item ``` For the most part, client code can treat a ListDownlink like an ordinary JavaScript list; the WARP client will ensure that the downlink is continuously made consistent with the remote lane. Using `didUpdate`, `didMove`, and `didRemove` callbacks, applications can update UI list views and other dependent components to keep them consistent with the shared state of the remote list lane in network real-time. From 6fc532e8845451da369742e96dc1d6f1ceb159d1 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 13 Mar 2024 10:30:06 -0700 Subject: [PATCH 16/17] Add various clarifying info and examples to FE docs --- src/_frontend/downlinks.md | 13 +++++++- src/_frontend/eventDownlink.md | 58 +++++++++++++++++++++++++++++---- src/_frontend/form.md | 4 +-- src/_frontend/gettingStarted.md | 4 +-- src/_frontend/mapDownlink.md | 4 +-- src/_frontend/valueDownlink.md | 4 +-- src/_frontend/warpClient.md | 2 +- 7 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/_frontend/downlinks.md b/src/_frontend/downlinks.md index 07ff184..3881e63 100644 --- a/src/_frontend/downlinks.md +++ b/src/_frontend/downlinks.md @@ -11,7 +11,7 @@ redirect_from: A Downlink provides a virtual bidirectional stream between the client and a lane of a remote Web Agent. WARP clients transparently multiplex all links to [**Web Agents**]({% link _backend/web-agents.md %}) on a given host over a single WebSocket connection. -Downlinks come in several flavors, depending on the WARP subprotocol to which they conform. A [**ValueDownlink**]({% link _frontend/valueDownlink.md %}) synchronizes a value with a remote value lane. A [**MapDownlink**]({% link _frontend/mapDownlink.md %}) implements the WARP map subprotocol to synchronize key-value state with a remote map lane. A [**ListDownlink**]({% link _frontend/listDownlink.md %}) implements the WARP list subprotocol to to synchronize sequential list state with a remote list lane. And an [**EventDownlink**]({% link _frontend/eventDownlink.md %}) observes raw WARP events, and can be used to observe lanes of any kind. +Downlinks come in several flavors, depending on the WARP subprotocol to which they conform. A [**ValueDownlink**]({% link _frontend/valueDownlink.md %}) synchronizes a value with a remote value lane. A [**MapDownlink**]({% link _frontend/mapDownlink.md %}) implements the WARP map subprotocol to synchronize key-value state with a remote map lane. And an [**EventDownlink**]({% link _frontend/eventDownlink.md %}) observes raw WARP events, and can be used to observe lanes of any kind. This article will focus on the properties and methods which all types of downlinks have in common. Later articles on specific types of downlinks will go into detail on what is unique to each of them. @@ -139,6 +139,17 @@ const downlink = client.current.downlink({ didDisconnect */ ``` +In the case that a downlink attempts to connect with a node (web agent) or lane which does not exist, the behavior would be almost exactly the same as the example above. The output from the callbacks would be identical, the only difference being that the "@unlinked" WARP message received by the client would include an error tag. + +```javascript +// WARP message received after network connection issue +"@unlink(node:\"/hotel/room/123\",lane:status)" +// WARP message received after web agent not found +"@unlinked(node:\"/hotel/room/invalid_room_number\",lane:status)@nodeNotFound" +// WARP message received after lane not found +"@unlinked(node:\"/hotel/room/123\",lane:invalid_lane_name)@laneNotFound" +``` + ### Linking and Syncing The `linked` method returns `true` if the logical WARP link is currently open. Changes to the link's state may be observed by registering callbacks with `willLink` or `didLink`. diff --git a/src/_frontend/eventDownlink.md b/src/_frontend/eventDownlink.md index 4eb9e1d..b992c32 100644 --- a/src/_frontend/eventDownlink.md +++ b/src/_frontend/eventDownlink.md @@ -9,30 +9,74 @@ redirect_from: {% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} -`EventDownlink` is not so much a subtype of `Downlink` as it is the base type of all downlinks. While not literally a superclass of `ValueDownlink`, `MapDownlink`, and `ListDownlink`, `EventDownlink` inherits from the same prototype as the others but contains no additional frills. For example, `MapDownlink` and `ListDownlink` support registering different callbacks for observing when a key-value pair has been added versus when one has been removed; and `ValueDownlink` has the `didSet` callback for when its value has been updated. `EventDownlink`, on the other hand, offers no specialized handling of WARP messages with respect to the type of Web Agent lane it is connected to. It provides a raw view of a WARP link, passing all received messages to a single `onEvent` callback. +`EventDownlink` is not so much a subtype of `Downlink` as it is the base type of all downlinks. While not literally a superclass of `ValueDownlink`, and `MapDownlink`, `EventDownlink` inherits from the same prototype as the others but contains no additional frills. For example, `MapDownlink` supports registering different callbacks for observing when a key-value pair has been added versus when one has been removed; and `ValueDownlink` has the `didSet` callback for when its synced value has been updated. `EventDownlink`, on the other hand, offers no specialized handling of WARP messages with respect to the type of Web Agent lane it is connected to. It provides a raw view of a WARP link, passing all received updates to a single `onEvent` callback. -Create an EventDownlink with a WARP client's `downlink` method. +Here is how to create a simple EventDownlink with a WARP client's `downlink` method. ```typescript import { WarpClient } from "@swim/client"; +const client = new WarpClient(); const downlink = client.downlink({ hostUri: "warp://example.com", - nodeUri: "/house", - laneUri: "power/meter", + nodeUri: "/house/electricityMeter", + laneUri: "stats" }) .open(); ``` -Using the `onEvent` callback, an application may update UI views and other dependent components in response to any messages received from the Web Agent. +Note the term that is used to refer to the kinds of events which trigger `onEvent`: "updates". Think of this as events which involve some state change. This could mean adding or removing keys to or from a map-based lane, or updating a ValueDownlink's synced value. WARP messages related to a downlink's connection state, such as those with the "link", "linked", "sync", "synced", "unlink", and "unlinked" tags, are not processed by the `onEvent` callback. `onEvent` is passed a single parameter of type [**Value**]( /frontend/structures#value ). + +An application may update UI views and other dependent components in response to any messages received from the Web Agent within the `onEvent` callback. Extending the EventDownlink created above, here is an example where we update some DOM element to display the amount of electricity a home has consumed this month. In this example, the remote web agent lane is a value lane. ```typescript -downlink.onEvent = (event) => { +downlink.onEvent = (value) => { + /* Raw WARP message + @event(node:"/house/electricityMeter",lane:stats){timestamp:1710272571408,currentReading:8432.7,prevMonthReading:7875.9,model:"Single Phase 4P Din Rail Energy Meter"} */ + // update UI view with latest value - document.getElementById("status").innerText = `The house has used ${event.get("powerConsumption").numberValue()} kWh.`; + const consumption = value.get("currentReading").numberValue() - value.get("prevMonthReading").numberValue(); + document.getElementById("meterDisplay").innerText = `This home has consumed ${consumption} kWh this month.`; }; ``` +Here is another example of an EventDownlink reacting to some updates. In this case, we are using them to keep track of a hotel's reservations, and the lane to which we've connected is a map lane. We can tell it is a map-based lane because the incoming messages each have an additional "update" or "remove" tag. The tag can be used to help us determine how the UI should be modified. + +```typescript +const downlink = client.downlink({ + hostUri: "warp://example.com", + nodeUri: "/hotel", + laneUri: "reservations", + onEvent: (value) => { + /* Example raw WARP messages + (update reservation) + Record.of(Attr.of("update", Record.of(Slot.of("key", "12345"))), Slot.of("arrival", "2024-04-01T15:00:00Z"), Slot.of("nights", 1), Slot.of("guestName", "Jeff Lebowski")) + (remove reservation) + Record.of(Attr.of("remove", Record.of(Slot.of("key", "67890")))) */ + + if (value.tag === "update") { + const id = value.get("update").get("key").stringValue(); + const arrival = value.get("arrival").stringValue(); + const nights = value.get("nights").numberValue(); + + if (existingReservations.find((r) => r.id === id)) { + updateExistingReservation({ id, arrival, nights }); + } else { + addNewReservation({ id, arrival, nights }); + } + } else if (value.tag === "remove") { + const id = value.get("remove").get("key").stringValue(); + + handleCancellation({ id }); + } else { + // tag type not recognized + return; + } + } +}) +.open(); +``` + ## Typescript The format of an EventDownlink's state is unconstrained, therefore, EventDownlink may not be passed any [**Forms**]({% link _frontend/form.md %}) and type annotation via that route is not supported. Any necessary typechecking must be done ad hoc within the `onEvent` callback. diff --git a/src/_frontend/form.md b/src/_frontend/form.md index 0b5e81e..5f55501 100644 --- a/src/_frontend/form.md +++ b/src/_frontend/form.md @@ -32,13 +32,13 @@ export class StockForm extends Form { override cast(item: Item): Stock | undefined { if ( item.tag === "update" && // make sure message is an update - item.getAttr("update").get("key").stringValue("") && // find key + item.get("update").get("key").stringValue("") && // find key item.get("price").isDefinite() && // ensure all fields are present item.get("volume").isDefinite() && item.get("movement").isDefinite() ) { return { - symbol: item.getAttr("update").get("key").stringValue(""), + symbol: item.get("update").get("key").stringValue(""), price: item.get("price").numberValue(0), volume: item.get("volume").numberValue(0), dailyChange: item.get("dailyChange").numberValue(0), diff --git a/src/_frontend/gettingStarted.md b/src/_frontend/gettingStarted.md index c706b48..0ce602a 100644 --- a/src/_frontend/gettingStarted.md +++ b/src/_frontend/gettingStarted.md @@ -59,7 +59,7 @@ All downlink variants support the ability to provide type information for incomi ## Quick Start -Connecting to a remote Web Agent with the WARP client can be done in just a few lines. +Connecting to a lane of a remote Web Agent with the WARP client can be done in just a few lines. Import and initialize an instance of `WarpClient`. @@ -75,7 +75,7 @@ Next, create a link for connecting to your remote Web Agent. const downlink = client.downlink(); ``` -Then provide your link with the URI of the Web Agent to which you wish to connect. +Then provide your link with the `nodeUri` and `laneUri of the Web Agent to which you wish to connect. ```typescript downlink.setHostUri("warp://example.com"); diff --git a/src/_frontend/mapDownlink.md b/src/_frontend/mapDownlink.md index 0479215..facc478 100644 --- a/src/_frontend/mapDownlink.md +++ b/src/_frontend/mapDownlink.md @@ -9,7 +9,7 @@ redirect_from: {% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} -A MapDownlink synchronizes a shared real-time key-value map with a remote [**map lane**]({% link _backend/map-lanes.md %}). MapDownlinks are also the downlink best suited for use with [**join value lanes**]. Much like a map lane, join value lanes are key-value maps where each value is itself a link to another value lane. In addition to the standard Downlink callbacks, MapDownlink supports registering `willUpdate`, `didUpdate`, `willRemove`, and `didRemove` callbacks to observe all changes to downlinked map state — whether remote or local. +A MapDownlink synchronizes a shared real-time key-value map with with any Web Agent lane backed by a map. In addition to [**map lanes**]({% link _backend/map-lanes.md %}), this includes [**join value lanes**]({% link _backend/join-value-lanes.md %}) and [**join map lanes**]({% link _backend/join-map-lanes.md %}), which are maps where each entry is its own value lane or maps lane, respectively. In addition to the standard Downlink callbacks, MapDownlink supports registering `didUpdate`, and `didRemove` callbacks for observing changes to downlinked map state — whether remote or local. `didUpdate` is invoked when an existing map key is updated or a new key is added. `didRemove` gets called when a map key is removed. Create a MapDownlink with a WARP client's `downlinkMap` method. @@ -46,7 +46,7 @@ mapDownlink.didRemove((key) => { ## State Type Disambiguation -A MapDownlink views its keys and values as [**Values**]({% link _frontend/structures.md %}) by default. Use the `keyForm` and `valueForm` methods to create a typed projection of a MapDownlink that automatically transforms its keys and values using [**Forms**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ValueDownlink's state to a boolean; and you can also use `Form.forAny()` to create a ValueDownlink that coerces its state to a plain old JavaScript value. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. +A MapDownlink views its keys and values as [**Values**]({% link _frontend/structures.md %}) by default. Provide an instance of a [**Form**]({% link _frontend/form.md %}) to the `keyForm` and/or `valueForm` options to create a typed projection of a MapDownlink that automatically transforms its keys and values, respectively, and provides type annotations. The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a MapDownlink's state to a boolean. `Form.forAny()` can be used to create a MapDownlink that coerces its state to an untyped JavaScript value; it's able to recognize primitive JavaScript values as well as arrays and plain old JavaScript objects, including nested objects. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. ```typescript const elevators = client.downlinkMap({ diff --git a/src/_frontend/valueDownlink.md b/src/_frontend/valueDownlink.md index 17b47f9..cd0cca5 100644 --- a/src/_frontend/valueDownlink.md +++ b/src/_frontend/valueDownlink.md @@ -9,7 +9,7 @@ redirect_from: {% include alert.html title='Version Note' text='This documentation describes Swim JS packages v4.0.0-dev-20230923 or later. Users of earlier package versions may experience differences in behavior.' %} -A ValueDownlink synchronizes a shared real-time value with a remote value lane. In addition to the standard Downlink callbacks, ValueDownlink supports registering `willSet` and `didSet` callbacks to observe all changes to downlinked state — whether remote or local. +A ValueDownlink synchronizes a shared real-time value with a remote value lane. In addition to the standard Downlink callbacks, ValueDownlink supports registering a `didSet` callback to observe changes to downlinked state — whether remote or local. Create a ValueDownlink with a WARP client's `downlinkValue` method. @@ -39,7 +39,7 @@ valueDownlink.didSet = (newValue) => { ## State Type Disambiguation -A ValueDownlink views its state as a [**Value**]({% link _frontend/structures.md %}) by default. Use the `valueForm` option to create a typed projection of a ValueDownlink that automatically transforms its state using a [**Form**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ValueDownlink's state to a boolean; and you can also use `Form.forAny()` to create a ValueDownlink that coerces its state to a plain old JavaScript value. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. +A ValueDownlink views its state as a [**Value**]({% link _frontend/structures.md %}) by default. Use the `valueForm` option to create a typed projection of a ValueDownlink that automatically transforms its state using a [**Form**]({% link _frontend/form.md %}). The `Form` class comes with a number of ready-to-use instances for basic use cases. For example, you can use `Form.forBoolean()` to coerce a ValueDownlink's state to a boolean. `Form.forAny()` can be used to create a MapDownlink that coerces its state to an untyped JavaScript value; it's able to recognize primitive JavaScript values as well as arrays and plain old JavaScript objects, including nested objects. Forms for coercing state to a string, number, `Value`, and `Item` are also provided. ```typescript const light = client.downlinkValue({ diff --git a/src/_frontend/warpClient.md b/src/_frontend/warpClient.md index 82e2525..3f9f5a1 100644 --- a/src/_frontend/warpClient.md +++ b/src/_frontend/warpClient.md @@ -35,7 +35,7 @@ const globalClient = WarpClient.global(); A [**downlink**]({% link _frontend/downlinks.md %}) provides a virtual bidirectional stream over which data can be synchronized between the client and a lane of a remote Web Agent. WARP clients transparently multiplex all links to Web Agents on a given host over a single WebSocket connection. A downlink represents one link in this scenario. -`WarpClient` includes four methods that open different kinds of downlinks. The `downlink` method creates an EventDownlink for streaming raw events from any Web Agent lane. The `valueDownlink` method creates a ValueDownlink for synchronizing state with a Web Agent value lane. The `mapDownlink` method creates a MapDownlink for synchronizing state with a Web Agent map lane. And the `listDownlink` method creates a ListDownlink for synchronizing state with a Web Agent list lane. +`WarpClient` includes three methods that open different kinds of downlinks. The `downlink` method creates an EventDownlink for streaming raw events from any Web Agent lane. The `valueDownlink` method creates a ValueDownlink for synchronizing state with a Web Agent [value lane]({% link _backend/value-lanes.md %}). A ValueDownlink views its state as a @swim/structure [**Value**](/frontend/structures#value) by default, which itself may represent any kind of JavaScript value, be it primitive or composite. `Value`s may be coerced into a strongly-typed value by passing a `Form` to the `valueForm` option. The `mapDownlink` method creates a MapDownlink. This type of downlink is useful for synchronizing state with any Web Agent lane backed by a map. In addition to [**map lanes**]({% link _backend/map-lanes.md %}), this includes [**join value lanes**]({% link _backend/join-value-lanes.md %}) and [**join map lanes**]({% link _backend/join-map-lanes.md %}), which are maps of other value lanes and maps lanes, respectively. Here is an example of opening an EventDownlink. We will go into further detail on all of the downlink types in subsequent sections. From 45906c79f75aea121a431f840c133ee0b258bdf6 Mon Sep 17 00:00:00 2001 From: Cameron James Date: Wed, 13 Mar 2024 16:48:23 -0700 Subject: [PATCH 17/17] Add a few more code examples and API descriptions to structures.md --- src/_frontend/structures.md | 61 +++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/_frontend/structures.md b/src/_frontend/structures.md index 2fc2447..840fe06 100644 --- a/src/_frontend/structures.md +++ b/src/_frontend/structures.md @@ -146,6 +146,51 @@ console.log(item.get("foo").booleanValue()) // undefined console.log(item.get("foo").booleanValue(false)) // false ``` +`toLike: ItemLike` + +Converts this `Item` into the most appropriate JavaScript object. It will parse out strings, numbers, booleans, Arrays, or plain old JavaScript object. Nested structures are supported. `Absent` is coerced to undefined and `Extant` is coerced to null. + +```typescript +// WARP message +// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} + +console.log(item.get('totalPrice').toLike()) // 29.98 +console.log(item.get('foo').toLike()) // undefined +console.log(item.header('update').toLike()) // { key: "/order/123456" } +console.log(item.toLike()) +/* { + @update: { key: "/order/123456" }, + timestamp: 1710295106760, + totalPrice: 29.98, + cart: [ + { + itemId: "/item/789012", + qty: 2, + unitPrice: 14.99 + } + ] + } */ +``` + +`static fromLike: Item` + +Converts a JavaScript object into an item. Essentially the reverse of `toLike`. Nested structures are supported. BigInt and Symbols types are not supported. Undefined is converted to `Absent` and null type is converted to `Extant`. Values of NaN are preserved. + +```typescript +console.log(Item.fromLike(null)) // Extant {} + +const obj = { + string: "Hello, world!", + number: "123456", + boolean: true, + nan: NaN, + array: ["a", 2, { three: 3 }], +}; +console.log(Item.fromLike().toString()) // Record.of(Slot.of("string", "Hello, world!"), Slot.of("number", "123456"), Slot.of("boolean", true), Slot.of("nan", NaN), Slot.of("array", Record.of("a", 2, Record.of(Slot.of("three", 3))))) +console.log(Item.fromLike().toLike()) // identical to original obj object + +``` + `readonly key: Value;` Returns the key component of this `Item`, if this `Item` is a Field; otherwise returns Absent if this `Item` is a `Value`. @@ -163,20 +208,20 @@ Used to concisely get the name of the discriminating attribute of a structure. T ```typescript /* - WARP message; update to value lane's synced value - @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} + WARP message; update to value lane's synced value + @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408} */ console.log(item.tag); // undefined /* - WARP message; key updated or added to map-based lane - @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} + WARP message; key updated or added to map-based lane + @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]} */ console.log(item.tag); // "update" /* - WARP message; key removed from map-based lane - @event(node:"/user/1234",lane:orders)@remove(key:"/order/456789") + WARP message; key removed from map-based lane + @event(node:"/user/1234",lane:orders)@remove(key:"/order/456789") */ console.log(item.tag); // "remove" ``` @@ -329,6 +374,10 @@ console.log(item.getItem(2).numberValue()) // 8432.7 console.log(item.getItem(99).stringValue()) // Absent {} ``` +`forEach(callback: (item: Item, index: number) => void)): undefined;` + +Iterates over every `Attribute` or `Slot` of an Item and executes the provided callback on it, receiving the individual `Attribute` or `Slot` and its index as arguments. If `forEach` is called on either `Absent` or an empty `Record`, the callback is never invoked as the `Item` does not contain a valid `Attribute` or `Slot`. If `forEach` is called on `Extant`, the callback gets invoked a single time with the `Extant` unit type. + `evaluate(interpreter: InterpreterLike): Item;` Returns a new `Item` with all nested expressions interpreted in lexical order and scope.