Skip to content

Commit c4b69a7

Browse files
committed
feat(IPC): Add client and server classes
1 parent 49ab1fc commit c4b69a7

17 files changed

Lines changed: 663 additions & 0 deletions

src/base/Client.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Client from './Client'
2+
import Request from './Request';
3+
import Response from './Response';
4+
5+
/**
6+
* Simple test client that implements the
7+
* necessary `send` method to echo back the
8+
* called method and parameters.
9+
*/
10+
class TestClient extends Client {
11+
send(request: Request) {
12+
const { id, method, params } = request
13+
const response = new Response(id, { method, params })
14+
this.receive(response)
15+
}
16+
}
17+
18+
test('client', async () => {
19+
const client = new TestClient()
20+
21+
expect(await client.capabilities()).toEqual({
22+
method: 'capabilities',
23+
params: []
24+
})
25+
26+
expect(await client.convert('{}')).toEqual({
27+
method: 'convert',
28+
params: ['{}', 'json', 'json']
29+
})
30+
31+
expect(await client.compile({})).toEqual({
32+
method: 'compile',
33+
params: [{}, 'json']
34+
})
35+
36+
expect(await client.build({})).toEqual({
37+
method: 'build',
38+
params: [{}, 'json']
39+
})
40+
41+
expect(await client.execute({})).toEqual({
42+
method: 'execute',
43+
params: [{}, 'json']
44+
})
45+
})

src/base/Client.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as stencila from '@stencila/schema';
2+
import Executor, { Method, Capabilities } from './Executor';
3+
import Request from './Request';
4+
import Response from './Response';
5+
6+
/**
7+
* A base client class which acts as a proxy to a remote `Executor`.
8+
*
9+
* Implements aynchronous, proxy methods for `Executor` methods `compile`, `build`, `execute`, etc.
10+
* Those methods send JSON-RPC requests to a `Server` that is serving the remote `Executor`.
11+
*/
12+
export default abstract class Client extends Executor {
13+
14+
/**
15+
* A map of requests to which responses can be paired against
16+
*/
17+
private requests: {[key: number]: (response: Request) => void } = {}
18+
19+
/**
20+
* Call the remote `Executor`'s `capabilities` method
21+
*/
22+
async capabilities (): Promise<Capabilities> {
23+
return this.call<Capabilities>(Method.capabilities)
24+
}
25+
26+
/**
27+
* Call the remote `Executor`'s `convert` method
28+
*/
29+
async convert (node: string | stencila.Node, from: string = 'json', to: string = 'json'): Promise<string> {
30+
return this.call<string>(Method.convert, node, from, to)
31+
}
32+
33+
/**
34+
* Call the remote `Executor`'s `compile` method
35+
*/
36+
async compile (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
37+
return this.call<stencila.Node>(Method.compile, node, format)
38+
}
39+
40+
/**
41+
* Call the remote `Executor`'s `build` method
42+
*/
43+
async build (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
44+
return this.call<stencila.Node>(Method.build, node, format)
45+
}
46+
47+
/**
48+
* Call the remote `Executor`'s `execute` method
49+
*/
50+
async execute (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
51+
return this.call<stencila.Node>(Method.execute, node, format)
52+
}
53+
54+
/**
55+
* Call a method of a remote `Executor`.
56+
*
57+
* @param method The name of the method
58+
* @param args Any method arguments
59+
*/
60+
private async call<Type> (method: Method, ...args: Array<any>): Promise<Type> {
61+
const request = new Request(method, args)
62+
const promise = new Promise<Type>((resolve, reject) => {
63+
this.requests[request.id] = (response: Response) => {
64+
if (response.error) return reject(new Error(response.error.message))
65+
resolve(response.result)
66+
}
67+
})
68+
this.send(request)
69+
return promise
70+
}
71+
72+
/**
73+
* Send a request to the server.
74+
*
75+
* This method must be overriden by derived client classes to
76+
* send the request over the transport used by that class.
77+
*
78+
* @param request The JSON-RPC request
79+
*/
80+
protected abstract send (request: Request): void
81+
82+
/**
83+
* Receive a response from the server.
84+
*
85+
* Usually called asynchronously via the `send` method of a derived class
86+
* when a response is returned. Uses the `id` of the response to match it to the corresponding
87+
* request and resolve it's promise.
88+
*
89+
* @param response The JSON-RPC response
90+
*/
91+
protected receive (response: string | Response): void {
92+
if (typeof response === 'string') response = JSON.parse(response) as Response
93+
if (response.id < 0) throw new Error(`Response is missing id: ${response}`)
94+
const resolve = this.requests[response.id]
95+
if (resolve === undefined) throw new Error(`No request found for response with id: ${response.id}`)
96+
resolve(response)
97+
delete this.requests[response.id]
98+
}
99+
}

src/base/Error.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* A JSON-RPC 2.0 response error
3+
*
4+
* @see {@link https://www.jsonrpc.org/specification#error_object}
5+
*/
6+
export default class JsonRpcError {
7+
/**
8+
* A Number that indicates the error type that occurred.
9+
* This MUST be an integer.
10+
*/
11+
code: number
12+
13+
/**
14+
* A String providing a short description of the error.
15+
* The message SHOULD be limited to a concise single sentence.
16+
*/
17+
message: string
18+
19+
/**
20+
* A Primitive or Structured value that contains additional information about the error.
21+
* This may be omitted.
22+
* The value of this member is defined by the Server (e.g. detailed error information,
23+
* nested errors etc.).
24+
*/
25+
data?: any
26+
27+
constructor (code: number, message: string, data?: any) {
28+
this.code = code
29+
this.message = message
30+
this.data = data
31+
}
32+
}

src/base/Executor.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as stencila from '@stencila/schema'
2+
3+
export enum Method {
4+
capabilities = 'capabilities',
5+
convert = 'convert',
6+
compile = 'compile',
7+
build = 'build',
8+
execute = 'execute'
9+
}
10+
11+
export type Capabilities = {[key in Method]: any}
12+
13+
/**
14+
*/
15+
export default class Executor {
16+
17+
/**
18+
* Get the capabilities of this executor
19+
*/
20+
async capabilities (): Promise<Capabilities> {
21+
return {
22+
capabilities: true,
23+
convert: false,
24+
compile: false,
25+
build: false,
26+
execute: false
27+
}
28+
}
29+
30+
async convert (node: string | stencila.Node, from: string = 'json', to: string = 'json'): Promise<string> {
31+
if (typeof node === 'string') return node
32+
else return JSON.stringify(node)
33+
}
34+
35+
async compile (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
36+
return node
37+
}
38+
39+
async build (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
40+
return node
41+
}
42+
43+
async execute (node: string | stencila.Node, format: string = 'json'): Promise<stencila.Node> {
44+
return node
45+
}
46+
}

src/base/Request.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* A JSON-RPC 2.0 request
3+
*
4+
* @see {@link https://www.jsonrpc.org/specification#request_object}
5+
*/
6+
export default class Request {
7+
/**
8+
* A string specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
9+
*/
10+
jsonrpc: string = '2.0'
11+
12+
/**
13+
* An identifier established by the Client that MUST contain a string, number, or
14+
* NULL value if included. If it is not included it is assumed to be a notification.
15+
* The value SHOULD normally not be Null and numbers SHOULD NOT contain fractional
16+
* parts. The Server MUST reply with the same value in the Response object if included.
17+
* This member is used to correlate the context between the two objects.
18+
*/
19+
id: number
20+
21+
/**
22+
* A string containing the name of the method to be invoked.
23+
* Method names that begin with the word rpc followed by a period character
24+
* (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and
25+
* MUST NOT be used for anything else.
26+
*/
27+
method?: string
28+
29+
/**
30+
* A structured value that holds the parameter values to be used during the
31+
* invocation of the method.This member MAY be omitted.
32+
*/
33+
params?: {[key: string]: any} | any[]
34+
35+
/**
36+
* A counter for generating unique, sequential request ids.
37+
*
38+
* Request ids don't need to be sequential but this helps with debugging.
39+
* Request ids don't need to be unique across clients.
40+
*/
41+
static counter: number = 0
42+
43+
constructor (method?: string, params?: {[key: string]: any} | any[]) {
44+
Request.counter += 1
45+
this.id = Request.counter
46+
this.method = method
47+
this.params = params
48+
}
49+
}

src/base/Response.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Error from './Error'
2+
3+
/**
4+
* A JSON-RPC 2.0 response
5+
*
6+
* @see {@link https://www.jsonrpc.org/specification#response_object}
7+
*/
8+
export default class Response {
9+
/**
10+
* A string specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
11+
*/
12+
jsonrpc: string = '2.0'
13+
14+
/**
15+
* This member is REQUIRED.
16+
* It MUST be the same as the value of the id member in the Request Object.
17+
* If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.
18+
*/
19+
id: number
20+
21+
/**
22+
* This member is REQUIRED on success.
23+
* This member MUST NOT exist if there was an error invoking the method.
24+
* The value of this member is determined by the method invoked on the Server.
25+
*/
26+
result?: any
27+
28+
/**
29+
* This member is REQUIRED on error.
30+
* This member MUST NOT exist if there was no error triggered during invocation.
31+
* The value for this member MUST be an Object as defined in section 5.1.
32+
*/
33+
error?: Error
34+
35+
constructor (id: number = -1, result?: any, error?: Error) {
36+
this.id = id
37+
this.result = result
38+
this.error = error
39+
}
40+
}

0 commit comments

Comments
 (0)