Skip to content

Commit a1ec214

Browse files
committed
feat(HTTP, Websocket): Add initial HTTP and Websocket clients and servers
1 parent bf360c5 commit a1ec214

22 files changed

Lines changed: 830 additions & 183 deletions

package-lock.json

Lines changed: 364 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
"@stencila/logga": "^1.3.0",
3131
"@stencila/schema": "^0.29.0",
3232
"ajv": "^6.10.2",
33+
"cross-fetch": "^3.0.4",
34+
"fastify": "^2.10.0",
35+
"fastify-websocket": "^0.6.0",
36+
"isomorphic-ws": "^4.0.1",
3337
"length-prefixed-stream": "^2.0.0",
3438
"minimist": "^1.2.0"
3539
},

src/base/Executor.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ describe('Peer', () => {
180180
capabilities: {},
181181
addresses: {
182182
http: {
183-
type: Transport.http
183+
type: Transport.http,
184+
host: '127.0.0.1',
185+
port: 8000
184186
}
185187
}
186188
},

src/base/Executor.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,16 +309,18 @@ export default class Executor implements Interface {
309309
* @param servers An array of `Server` instances that pass
310310
* requests on to this executor
311311
*/
312-
public start(servers: Server[] = []): void {
312+
public async start(servers: Server[] = []): Promise<void[]> {
313313
this.servers = servers
314-
log.info('Starting servers')
315-
this.servers.forEach(server => server.start())
314+
log.info(
315+
`Starting servers: ${this.servers.map(server => server.address.type)}`
316+
)
317+
return Promise.all(this.servers.map(server => server.start()))
316318
}
317319

318320
/**
319321
* Stop servers for the executor.
320322
*/
321-
public stop(): void {
323+
public async stop(): Promise<void> {
322324
log.info('Stopping servers')
323325
this.servers.forEach(server => server.stop())
324326
}
@@ -388,7 +390,7 @@ export default class Executor implements Interface {
388390
*/
389391
protected async addresses(): Promise<Addresses> {
390392
return this.servers
391-
.map(server => server.address())
393+
.map(server => server.address)
392394
.reduce((prev, curr) => ({ ...prev, ...{ [curr.type]: curr } }), {})
393395
}
394396

src/base/Server.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ export default abstract class Server {
1414
*/
1515
private executor: Executor
1616

17+
public abstract readonly address: Address
18+
1719
public constructor(executor?: Executor) {
1820
if (executor === undefined) executor = new Executor()
1921
this.executor = executor
2022
}
2123

22-
public abstract address(): Address
23-
2424
/**
2525
* Handle a request
2626
*
@@ -54,8 +54,12 @@ export default abstract class Server {
5454
}
5555

5656
try {
57+
if (request === null) {
58+
throw new Error(-32600, `Invalid request`)
59+
}
60+
5761
if (typeof request === 'string') {
58-
// Parse JSON into an request
62+
// Parse JSON into a request
5963
try {
6064
request = JSON.parse(request) as Request
6165
} catch (err) {
@@ -113,22 +117,24 @@ export default abstract class Server {
113117
/**
114118
* Start the server
115119
*/
116-
public start(): void {}
120+
public async start(): Promise<void> {}
117121

118122
/**
119123
* Stop the server
120124
*/
121-
public stop(): void {}
125+
public async stop(): Promise<void> {}
122126

123127
/**
124128
* Run the server with graceful shutdown on `SIGINT` or `SIGTERM`
125129
*/
126-
public run(): void {
130+
public async run(): Promise<void> {
127131
if (process !== undefined) {
128-
const stop = (): void => this.stop()
132+
const stop = async (): Promise<void> => {
133+
await this.stop()
134+
}
129135
process.on('SIGINT', stop)
130136
process.on('SIGTERM', stop)
131137
}
132-
this.start()
138+
await this.start()
133139
}
134140
}

src/base/Transports.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { tcpAddress } from './Transports'
1+
import { TcpAddress } from './Transports'
22

33
describe('tcpAddress', () => {
44
const defaults = {
@@ -7,21 +7,24 @@ describe('tcpAddress', () => {
77
}
88

99
test('parses a string with host and port', () => {
10-
expect(tcpAddress('example.com:2010', defaults)).toEqual({
10+
expect(new TcpAddress('example.com:2010', defaults)).toEqual({
11+
type: 'tcp',
1112
host: 'example.com',
1213
port: 2010
1314
})
1415
})
1516

1617
test('parses a number as the port', () => {
17-
expect(tcpAddress(2020, defaults)).toEqual({
18+
expect(new TcpAddress(2020, defaults)).toEqual({
19+
type: 'tcp',
1820
host: '127.0.0.1',
1921
port: 2020
2022
})
2123
})
2224

2325
test('parses a string with just port', () => {
24-
expect(tcpAddress('2030', defaults)).toEqual({
26+
expect(new TcpAddress('2030', defaults)).toEqual({
27+
type: 'tcp',
2528
host: '127.0.0.1',
2629
port: 2030
2730
})

src/base/Transports.ts

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,51 +25,86 @@ export interface VsockAddress {
2525
cid?: number
2626
}
2727

28-
export interface TcpAddress {
29-
type: Transport.tcp | Transport.http | Transport.ws
30-
host?: string
31-
port?: number
32-
}
28+
export type TcpAddressInitializer =
29+
| undefined
30+
| number
31+
| string
32+
| { host: string; port: number }
33+
| TcpAddress
34+
export class TcpAddress {
35+
public readonly type: Transport.tcp | Transport.http | Transport.ws =
36+
Transport.tcp
3337

34-
export function tcpAddress(
35-
address: undefined | string | number | Omit<TcpAddress, 'type'>,
36-
defaults: {
37-
host: string
38-
port: number
39-
}
40-
): Required<Omit<TcpAddress, 'type'>> {
41-
if (address === undefined) {
42-
return defaults
43-
} else if (typeof address === 'string') {
44-
const parts = address.split(':')
45-
if (parts.length === 1) {
46-
return {
47-
host: defaults.host,
48-
port: parseInt(parts[0])
49-
}
50-
} else if (parts.length >= 2) {
51-
return {
52-
host: parts[0],
53-
port: parseInt(parts[1])
54-
}
55-
} else {
56-
return defaults
38+
public readonly host: string
39+
40+
public readonly port: number
41+
42+
public constructor(
43+
address?: TcpAddressInitializer,
44+
defaults: {
45+
host: string
46+
port: number
47+
} = {
48+
host: '127.0.0.1',
49+
port: 2000
5750
}
58-
} else if (typeof address === 'number') {
59-
return { host: defaults.host, port: address }
60-
} else {
61-
const { host = defaults.host, port = defaults.port } = address
62-
return { host, port }
51+
) {
52+
const { host, port } = (function() {
53+
if (address === undefined) {
54+
return defaults
55+
} else if (typeof address === 'string') {
56+
const parts = address.split(':')
57+
if (parts.length === 1) {
58+
return {
59+
host: defaults.host,
60+
port: parseInt(parts[0])
61+
}
62+
} else if (parts.length >= 2) {
63+
return {
64+
host: parts[0],
65+
port: parseInt(parts[1])
66+
}
67+
} else {
68+
return defaults
69+
}
70+
} else if (typeof address === 'number') {
71+
return { host: defaults.host, port: address }
72+
} else {
73+
const { host = defaults.host, port = defaults.port } = address
74+
return { host, port }
75+
}
76+
})()
77+
if (address instanceof TcpAddress) this.type = address.type
78+
this.host = host
79+
this.port = port
80+
}
81+
82+
public toString(): string {
83+
return `${this.type}://${this.host}:${this.port}`
6384
}
6485
}
6586

66-
export interface HttpAddress extends TcpAddress {
67-
type: Transport.http | Transport.ws
68-
jwt?: string
87+
export class HttpAddress extends TcpAddress {
88+
public readonly type: Transport.http | Transport.ws = Transport.http
89+
90+
public readonly jwt?: string
91+
92+
public constructor(
93+
address?: TcpAddressInitializer,
94+
defaults: {
95+
host: string
96+
port: number
97+
} = {
98+
host: '127.0.0.1',
99+
port: 8000
100+
}
101+
) {
102+
super(address, defaults)
103+
}
69104
}
70105

71-
export interface WebsocketAddress extends HttpAddress {
72-
type: Transport.ws
106+
export class WebSocketAddress extends HttpAddress {
107+
public readonly type: Transport.ws = Transport.ws
73108
}
74109

75110
export type Address =
@@ -78,4 +113,4 @@ export type Address =
78113
| VsockAddress
79114
| TcpAddress
80115
| HttpAddress
81-
| WebsocketAddress
116+
| WebSocketAddress

src/cli.ts

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { defaultHandler, getLogger, LogLevel, replaceHandlers } from '@stencila/logga';
2-
import fs from 'fs';
3-
import minimist from 'minimist';
4-
import { promisify } from 'util';
5-
import { ClientType } from './base/Client';
6-
import Executor from './base/Executor';
7-
import Server from './base/Server';
8-
import discoverStdio from './stdio/discover';
9-
import StdioClient from './stdio/StdioClient';
10-
import TcpServer from './tcp/TcpServer';
11-
12-
const readFile = promisify(fs.readFile)
13-
const writeFile = promisify(fs.writeFile)
1+
import {
2+
defaultHandler,
3+
getLogger,
4+
LogLevel,
5+
replaceHandlers
6+
} from '@stencila/logga'
7+
import minimist from 'minimist'
8+
import { ClientType } from './base/Client'
9+
import Executor from './base/Executor'
10+
import Server from './base/Server'
11+
import discoverStdio from './stdio/discover'
12+
import StdioClient from './stdio/StdioClient'
13+
import HttpServer from './http/HttpServer'
14+
import TcpServer from './tcp/TcpServer'
15+
import WebSocketServer from './ws/WebSocketServer'
1416

1517
const { _: args, ...options } = minimist(process.argv.slice(2))
1618

@@ -56,51 +58,47 @@ const init = async () => {
5658
/**
5759
* Serve the executor
5860
*/
59-
const serve = (executor: Executor) => {
61+
const serve = async (executor: Executor) => {
6062
// Add server classes based on supplied options
6163
const servers: Server[] = []
6264
if (options.tcp !== undefined) {
6365
servers.push(new TcpServer(executor, options.tcp))
6466
}
67+
if (options.http !== undefined) {
68+
servers.push(new HttpServer(executor, options.http))
69+
}
70+
if (options.ws !== undefined) {
71+
servers.push(new WebSocketServer(executor, options.ws))
72+
}
6573
if (servers.length === 0) {
6674
log.warn(
6775
'No servers specified in options (e.g. --tcp --stdio). Executor will not be accessible.'
6876
)
6977
}
70-
executor.start(servers)
78+
await executor.start(servers)
7179
}
7280

7381
/**
7482
* Convert a document
7583
*/
7684
const convert = async (executor: Executor): Promise<void> => {
7785
const input = args[1]
78-
const output = args[2] || '-'
79-
80-
const content = await readFile(input, 'utf8')
86+
const output = args[2] !== undefined ? args[2] : '-'
8187

82-
const decoded = await executor.decode(content)
83-
const encoded = await executor.encode(decoded)
84-
85-
if (output === '-') console.log(encoded)
86-
else await writeFile(output, encoded)
88+
const decoded = await executor.decode(input)
89+
await executor.encode(decoded, output)
8790
}
8891

8992
/**
9093
* Execute a document
9194
*/
9295
const execute = async (executor: Executor): Promise<void> => {
9396
const input = args[1]
94-
const output = args[2] || input
95-
96-
const content = await readFile(input, 'utf8')
97+
const output = args[2] !== undefined ? args[2] : input
9798

98-
const decoded = await executor.decode(content)
99+
const decoded = await executor.decode(input)
99100
const executed = await executor.execute(decoded)
100-
const encoded = await executor.encode(executed)
101-
102-
if (output === '-') console.log(encoded)
103-
else await writeFile(output, encoded)
101+
await executor.encode(executed, output)
104102
}
105103

106104
// Run the main function and log any exceptions

src/direct/DirectClientServer.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Executor from '../base/Executor'
66
describe('DirectClient and DirectServer', () => {
77
const executor = new Executor()
88
const server = new DirectServer(executor)
9-
const client = new DirectClient(server.address())
9+
const client = new DirectClient(server.address)
1010

1111
test('calling manifest', async () => {
1212
expect(await client.manifest()).toEqual(await executor.manifest())
@@ -27,6 +27,4 @@ describe('DirectClient and DirectServer', () => {
2727
// @ts-ignore
2828
expect(Object.keys(client.requests).length).toEqual(0)
2929
})
30-
31-
server.stop()
3230
})

0 commit comments

Comments
 (0)