-
Notifications
You must be signed in to change notification settings - Fork 0
Readme
This README is based heavily on this wiki page from Anson Miu's project STScript.
The artifact comes with three case-studies implemented using CAScript:
OnlineWallet, Adder, and Contract. For each, you can run a build script from ~
.
$ build_onlinewallet
This will:
- Generate the TypeScript files, using CAScript, by reading from the
protocols
folder. - Install Node dependencies for the applications, for both the browsers and web server.
- Compile the application.
These scripts are in the scripts
folder.
Once you have run a case-study's build script, you can instantiate the server for it. For OnlineWallet, for example, you would do:
$ cd ~/case-studies/OnlineWallet
$ npm start
Then, the single-page application would be accessible at https://localhost:8080/.
The rest of this document walks through how to generate TypeScript APIs for a simple Adder web-service. The artifact already comes with an implementation of Adder. This document's one is slightly simpler.
In the Adder web-service, clients either:
- Submit two integers to the server, and receive the sum; or
- Request to exit the service.
The Adder protocol can be located under
~/protocols/Adder.scr
.
You can use the other web applications
under ~/case-studies
as a template,
but this guide will walk through the steps
from scratch.
Note: This guide will require you to write new source code files
using a terminal editor. The Docker image comes with vim
and nano
installed. If you wish to install additional software for editing, obtain
sudo access with the password dev
.
We will create the Adder web-service under
the ~/case-studies
directory.
This will hold both the server and client roles.
Run the following commands to initialise the Adder server as a Node application using TypeScript.
$ mkdir -p ~/case-studies/SimplerAdder
$ cd ~/case-studies/SimplerAdder
Run the following commands to copy the package.json
and tsconfig.json
files from the OnlineWallet application,
as the dependencies will be the same.
We need the build scripts and dependencies from
package.json
, and the TypeScript project configurations fromtsconfig.json
.
Optional:
You may also change the name
and description
fields of the
package.json
file to refer to the Adder web-service.
$ cd /home/dev/case-studies/SimplerAdder
$ cp ../OnlineWallet/package.json ../OnlineWallet/tsconfig.json ./
Run the following commands to set up the Node project.
$ cd /home/dev/case-studies/SimplerAdder
# Source code for server will go inside 'src'
$ mkdir src
# Install dependencies
$ npm i
Run the following commands to initialise the SimplerAdder client as a React application using TypeScript.
$ cd /home/dev/case-studies/SimplerAdder
# Create React application with TypeScript project setup.
# This will take a couple of minutes...
$ npx create-react-app client --template typescript
# Install dependencies
$ cd client && npm i
We have the minimal scaffolding of the Adder web-service.
Run the following commands to generate the APIs for the
server.
The APIs will be generated under /home/dev/case-studies/Adder/src
.
$ cd /home/dev/case-studies/SimplerAdder
$ python -m codegen \
~/protocols/Adder.scr \ # use this Scribble file
Adder \ # generate Adder protocol
Svr \ # for Svr role
node \ # targeting Node endpoint
-o src \ # output to `src` path
Run the following commands to generate the APIs for the
client.
The APIs will be generated under /home/dev/case-studies/SimplerAdder/client/src
.
# Note that this is the "client" subdirectory inside Adder.
$ cd /home/dev/case-studies/SimplerAdder/client
^^^^^^
$ python -m codegen \
~/protocols/Adder.scr \ # use this Scribble file
Adder \ # generate Adder protocol
Client \ # for Client role
browser \ # targeting browser endpoint
-s Svr \ # with `Svr` as server role
-o src \ # output to `src` path
We have the TypeScript APIs required for a communication-safe implementation of the SimplerAdder web-service.
We proceed to write the application logic for both endpoints, leveraging the generated APIs to guarantee communication-safety by construction.
Create the file, /home/dev/case-studies/SimplerAdder/src/index.ts
, and add the following file content.
You may omit the comments, they are included here simply to guide you and provide context.
Here, we are simulating a 2-second delay at the server role when computing the sum. This property allows us to assess how our client endpoint responds to network delays and verify that channel reuse is impossible even under these conditions.
// =============
// Set up server
// =============
import express from "express";
import path from "path";
import http from "http";
import WebSocket from "ws";
const app = express();
// Serve client-side static files.
app.use(express.static(path.join(__dirname, "client")));
// Create WebSocket server.
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// ==================
// Implement protocol
// ==================
import {
Session, // Holds named constructors for main EFSM states.
Svr, // Constructor for session runtime.
} from "./Adder/Svr";
import { Factory, Message } from './Adder/Svr/EFSM';
// This function performs the sum after a simulated 2 second delay
const sumAfterTwoSeconds = (x: number, y: number) => new Promise<number>((resolve, reject) => {
setTimeout(() => resolve(x + y), 2000);
});
const adderServerLogic = (sessionID: string) => {
const handleRequest = Session.Initial({
ADD: async (Next: typeof Factory.S1, payload: Message.S0_ADD_payload) => {
// `Next` is a named constructor for successor states,
// which is useful for code-completion in editors/IDEs.
// `x` and `y` are inferred to be numbers.
const { x, y } = payload;
const res = await sumAfterTwoSeconds(x, y);
// Send the RES message with the sum, and continue
// onto the initial state by recursion.
return Next.RES({ res }, handleRequest)
},
QUIT: Session.Terminal
});
return handleRequest;
};
// ============
// Execute EFSM
// ============
new Svr(
wss,
(sessionID, role, reason) => {
// Simple cancellation handler
console.error(`${sessionID}: ${role} cancelled session because of ${reason}`);
},
adderServerLogic,
);
const PORT = process.env.PORT ?? 8080;
server.listen(PORT, () => {
console.log(`Listening on port ${PORT}...`);
});
We proceed to implement the React application for the Client endpoint.
We need to implement a React component for each state of the Client's EFSM.
These states are located inside /home/dev/case-studies/SimplerAdder/client/src/Adder/Client
,
and are prefixed with S__.tsx
, as shown below.
The
.tsx
file extension denotes that the TypeScript source file also uses the JSX syntax extension.
There should be only three state components generated.
By convention, we will group our component implementations in a subdirectory.
$ mkdir /home/dev/case-studies/SimplerAdder/client/src/components
Each of the generated EFSM state components define an abstract React class component. To instantiate the session runtime for the web application, we need to define custom implementations of each state component.
The Client's EFSM defines three states:
- A send state to either send
ADD
orQUIT
messages (which is also the initial state); - A receive state pending the receipt of the
RES
message with the sum; - A terminal state after sending the
QUIT
message.
Inspect each of the generated EFSM state components to figure out which state identifier corresponds to which state.
The send state corresponds to S0.tsx
.
$ cat /home/dev/case-studies/Adder/client/src/Adder/Client/S0.tsx
/* ...snip... */
/**
* __Send state: Possible messages:
*
* * __Sends to Svr, ADD__(Payloads.ADD)
* * __Sends to Svr, QUIT__({})
*/
export default abstract class S0<ComponentState = {}> extends React.Component<Props, ComponentState>
{
/* ...snip... */
Create a React class component that inherits from this abstract base class.
Here we name the component SelectionScreen.tsx
.
$ cd /home/dev/case-studies/SimplerAdder/client/src/components
$ touch SelectionScreen.tsx
Implement the SelectionScreen
component.
The sample code below displays two text boxes to keep track of the operands
for the ADD
message, and exposes two buttons labelled Submit and Quit Adder
to trigger the ADD
and QUIT
selections respectively.
For simplicity, we will use the
localStorage
API to keep track of the operands submitted to the Adder service. The other case studies use the React Context API to propagate the shared data.
// SelectionScreen.tsx
import React from 'react';
import S0 from '../Adder/Client/S0';
// This UI component needs to keep track of the numbers entered
// by the user. React components can keep track of mutable state.
type ComponentState = {
x: number,
y: number,
};
export default class SelectionScreen extends S0<ComponentState> {
// To update the internal state, we call `this.setState(obj)`,
// where `obj` is an object literal mapping property names
// to their updated values.
state = {
x: 0,
y: 0,
};
// The `render` method corresponds to the view function
// explained in the literature. The view function returns the UI
// components to be rendered on the DOM.
render() {
// `this.ADD` is a component factory that takes a DOM event and a
// callback and returns a React component with the IO action bound
// inside. Here, we are building a React component named 'Submit' such that,
// when the user "clicks" on it, the callback is triggered and returns the
// payload to send along with the "ADD" message. The payload must be typed as
// a tuple of two numbers, as per the Scribble protocol specification.
const Submit = this.ADD('onClick', ev => {
const { x, y } = this.state;
localStorage.setItem('operands', `${x},${y}`);
return { x, y };
});
// Likewise, `this.ADD` generates a React component that, when clicked on,
// sends the "QUIT" message with no payload (denoted by empty tuple).
const Quit = this.QUIT('onClick', ev => {
return [];
});
return (
<div>
<input
type='number'
placeholder='Num1'
// When the user changes their input in this textbox, interpret
// the input value as a Number and update the entry for `num1` in
// the component state. This allows `this.ADD` to access the latest value
// when sending the "ADD" message.
onChange={(ev) => this.setState({ x: Number(ev.target.value) })}
/>
<input
type='number'
placeholder='Num2'
onChange={(ev) => this.setState({ y: Number(ev.target.value) })}
/>
<Submit>
{
// Any UI component inside the "Submit" component will inherit the
// event listeners in the parent, so when the user clicks on the button labelled
// 'Submit', the behaviour for `this.ADD` described above will be triggered.
}
<button>Submit</button>
</Submit>
<hr />
<Quit>
<button>Quit Adder</button>
</Quit>
</div>
);
}
};
The receive state corresponds to S1.tsx
.
$ cat /home/dev/case-studies/Adder/client/src/Adder/Client/S1.tsx
/* ...snip... */
/**
* __Receives from Svr.__ Possible messages:
*
* * __RES__(Payloads.RES)
*/
export default abstract class S1<ComponentState = {}> extends React.Component<Props, ComponentState>
{
/* ...snip... */
abstract RES(payload: Payloads.RES): MaybePromise<void>;
};
Note: The abstract class defines an abstract method RES
,
which functions as a hook,
as it is invoked by the session runtime once the message is received.
Create a React class component that inherits from this abstract base class.
Here we name the component WaitingScreen.tsx
.
Recall that the SimplerAdder server takes 2 seconds to respond to an addition request. This React component will be rendered whilst waiting to receive the response from the Adder server, hence it is aptly named "WaitingScreen".
$ cd /home/dev/case-studies/SimplerAdder/client/src/components
$ touch WaitingScreen.tsx
Implement the WaitingScreen
component.
The sample code below displays some waiting text, and when the RES
message is received, it displays an alert that logs the sum of the operands.
The operands are accessed through the localStorage
API in the same way
that they were stored through the SelectionScreen.tsx
component.
// WaitingScreen.tsx
import React from 'react';
import S1, { Payloads } from '../Adder/Client/S1';
export default class WaitingScreen extends S1 {
RES(payload: Payloads.RES) {
// Fetch operands from localStorage API.
const [x, y] = localStorage
.getItem('operands')!
.split(',')
.map(x => Number(x)) as [number, number];
// Display alert to user
alert(`${x} + ${y} = ${payload.res}`);
}
render() {
return (
<div>
<h2>Waiting for sum...</h2>
</div>
);
}
};
The terminal state corresponds to S2.tsx
.
$ cat /home/dev/case-studies/Adder/client/src/Adder/Client/S2.tsx
/* ...snip... */
/**
* __Terminal state__.
*/
export default abstract class S2<ComponentState = {}> extends React.Component<Props, ComponentState> {
componentDidMount() {
this.props.terminate();
}
}
Create a React class component that inherits from this abstract base class.
Here we name the component EndScreen.tsx
.
$ cd /home/dev/case-studies/SimplerAdder/client/src/components
$ touch EndScreen.tsx
Implement the EndScreen
component.
The sample code below displays a helpful message and nothing more.
More complex web applications can leverage the useEffect
hook to perform any required clean-up when the session terminates.
// EndScreen.tsx
import React from 'react';
import S2 from '../Adder/Client/S2';
export default class EndScreen extends S2 {
render() {
return (
<div>
<h2>Closed Connection to Adder Service</h2>
<p>Please refresh the window to connect again.</p>
</div>
);
}
}
React components define a hierarchy of UI views.
By construction from the create-react-app
scaffolding, the
top of the hierarchy is defined in
/home/dev/case-studies/SimplerAdder/client/src/App.tsx
.
Modify this component so that the main application renders the session runtime of the Adder protocol for the Client role. The instantiation of the session runtime requires some additional React components, which we label with footnotes in the code and explain later.
The session runtime itself is also a React component. It is responsible for:
- keeping track of the current EFSM state;
- rendering the custom implementation of the corresponding EFSM state; and
- ensuring that the IO actions made available on the DOM only corresponds to the permitted transitions in the current EFSM state.
// App.tsx
import React from 'react';
import './App.css';
// Import the session runtime component.
import { Client } from './Adder/Client';
// Import our custom state implementations.
import SelectionScreen from './components/SelectionScreen';
import WaitingScreen from './components/WaitingScreen';
import EndScreen from './components/EndScreen';
function App() {
const origin = window.location.origin;
const endpoint = origin.replace(/^http/, 'ws');
return (
<div className='App'>
<h1>Simpler Adder Service</h1>
<Client
endpoint={endpoint}
// Map each state identifier to the corresponding
// concrete state implementation.
states={{
S0: SelectionScreen,
S1: WaitingScreen,
S2: EndScreen,
}}
// {1}
waiting={
<div>
<p>Connection to Server...</p>
</div>
}
// {2}
connectFailed={
<div>
<p>ERROR: cannot connect to Server...</p>
</div>
}
// {3}
cancellation={(role, reason) => {
return (
<p>{role} cancelled session due to {reason}</p>
);
}}
/>
</div>
);
}
export default App;
-
{1} defines the UI to be displayed when waiting for the session to start.
-
{2} defines the UI to be displayed if the Client fails to connect to the server.
-
{3} defines a function that is invoked whenever the session is cancelled before reaching the terminal state (e.g. if the Server goes down), and is parameterised with the role that cancelled the session and the reason (if any). This function renders the UI to be displayed when the session is cancelled.
We are now in a position to test the SimplerAdder web-service. Run the following command to build both the server and client endpoints. The TypeScript Compiler is executed to check for typing violations, and upon success, compiles both endpoints into JavaScript code that can be executed by the Node.js runtime and the browser respectively.
$ cd /home/dev/case-studies/SimplerAdder
$ npm run build
Run the following command to start the Adder web-service.
$ cd /home/dev/case-studies/SimplerAdder
$ npm start
Navigate to http://localhost:8080
on your browser to
use the SimplerAdder service.
Observe that, even though there is a delay between sending the
ADD
message and receiving the RES
message, by construction
from the generated APIs, it is impossible for the client to
submit another ADD
message whilst waiting for the RES
message.
Congratulations, you have implemented your first communication-safe web application!
We list some ideas for extending the Adder web-service with modern web programming practices:
- Modernise the UI using external libraries. Two popular examples are Material-UI and React-Bootstrap.
- Display the sum on the DOM rather than an alert. You can refer to the other case studies for how to propagate state using the React Context API, as well as how to extract common UI elements in individual components to reuse across different EFSM states.
Both of these ideas are implemented in the more complex Adder found in /home/dev/case-studies/Adder