@service-broker/webclient 1.0.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -8
- package/dist/index.d.ts +31 -26
- package/dist/index.js +183 -182
- package/package.json +7 -4
- package/src/index.ts +225 -0
- package/tsconfig.json +15 -0
package/README.md
CHANGED
|
@@ -1,15 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
Browser
|
|
1
|
+
## @service-broker/webclient
|
|
2
|
+
Browser ESM client library for communicating with a [Service Broker](https://github.com/service-broker/service-broker/wiki/specification)
|
|
3
3
|
|
|
4
|
-
## Install
|
|
5
|
-
`npm install @service-broker/webclient`
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
### Install
|
|
6
|
+
```bash
|
|
7
|
+
npm install @service-broker/webclient
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Connect
|
|
12
|
+
Connect to the Service Broker at the specified WebSocket URL.
|
|
13
|
+
|
|
8
14
|
```javascript
|
|
9
15
|
import { ServiceBroker } from "@service-broker/webclient"
|
|
10
16
|
|
|
11
|
-
const sb = new ServiceBroker("wss://sb.mydomain.com"
|
|
17
|
+
const sb = new ServiceBroker("wss://sb.mydomain.com")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Request
|
|
22
|
+
Send a service request. The broker will select a qualified provider based on service `name` and requested `capabilities`. The parameter `request` contains the actual message that'll be delivered to the service provider.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
interface Message {
|
|
26
|
+
header: {
|
|
27
|
+
from: string // the endpointId of the sender
|
|
28
|
+
to: string // the endpointId of the recipient
|
|
29
|
+
},
|
|
30
|
+
payload: string // the message payload, usually JSON
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
sb.request(
|
|
34
|
+
service: {
|
|
35
|
+
name: string,
|
|
36
|
+
capabilities?: string[]
|
|
37
|
+
},
|
|
38
|
+
request: Message,
|
|
39
|
+
timeout?: number
|
|
40
|
+
): Promise<Message>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Notify
|
|
45
|
+
A notification is like a request except no response will be sent back.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
sb.notify(
|
|
49
|
+
service: {
|
|
50
|
+
name: string,
|
|
51
|
+
capabilities?: string[]
|
|
52
|
+
},
|
|
53
|
+
notification: Message
|
|
54
|
+
): Promise<void>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### RequestTo
|
|
59
|
+
Send a service request directly to an endpoint.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
sb.requestTo(
|
|
63
|
+
endpointId: string,
|
|
64
|
+
serviceName: string,
|
|
65
|
+
request: Message,
|
|
66
|
+
timeout?: number
|
|
67
|
+
): Promise<Message>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
### NotifyTo
|
|
72
|
+
Send a notification directly to an endpoint.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
sb.notifyTo(
|
|
76
|
+
endpointId: string,
|
|
77
|
+
serviceName: string,
|
|
78
|
+
notification: Message
|
|
79
|
+
): Promise<void>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
### SetServiceHandler
|
|
84
|
+
The `requestTo` and `notifyTo` methods can be used to send direct messages to an endpoint. For example, a chat service provider may publish a client's endpointId to other clients and allow them to send direct messages to one another.
|
|
85
|
+
|
|
86
|
+
This method sets a handler for incoming requests and notifications.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
sb.setServiceHandler(
|
|
90
|
+
serviceName: string,
|
|
91
|
+
handler: (request: Message) => Message|void|Promise<Message|void>
|
|
92
|
+
): void
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### Publish/Subscribe
|
|
97
|
+
```typescript
|
|
98
|
+
sb.publish(
|
|
99
|
+
topic: string,
|
|
100
|
+
text: string
|
|
101
|
+
): Promise<void>
|
|
102
|
+
|
|
103
|
+
sb.subscribe(
|
|
104
|
+
topic: string,
|
|
105
|
+
handler: (text: string) => void
|
|
106
|
+
): Promise<void>
|
|
12
107
|
|
|
13
|
-
sb.
|
|
14
|
-
|
|
108
|
+
sb.unsubscribe(
|
|
109
|
+
topic: string
|
|
110
|
+
): Promise<void>
|
|
15
111
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
|
|
2
1
|
interface ServiceFilter {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
name: string;
|
|
3
|
+
capabilities?: string[];
|
|
5
4
|
}
|
|
6
|
-
|
|
7
|
-
export interface ServiceAdvert {
|
|
8
|
-
name: string
|
|
9
|
-
capabilities?: string[]
|
|
10
|
-
priority: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
5
|
export interface ServiceHandler {
|
|
14
|
-
|
|
6
|
+
(request: Message): Message | void | Promise<Message | void>;
|
|
15
7
|
}
|
|
16
|
-
|
|
17
8
|
interface Message {
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
header?: Record<string, unknown>;
|
|
10
|
+
payload?: string;
|
|
20
11
|
}
|
|
21
|
-
|
|
22
12
|
export declare class ServiceBroker {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
private readonly url;
|
|
14
|
+
private readonly providers;
|
|
15
|
+
private ws;
|
|
16
|
+
private readonly connectListeners;
|
|
17
|
+
private pendingSend;
|
|
18
|
+
private readonly pendingResponses;
|
|
19
|
+
private pendingIdGen;
|
|
20
|
+
constructor(url: string);
|
|
21
|
+
private connect;
|
|
22
|
+
private onOpen;
|
|
23
|
+
private onClose;
|
|
24
|
+
private onMessage;
|
|
25
|
+
private onServiceResponse;
|
|
26
|
+
private onServiceRequest;
|
|
27
|
+
private send;
|
|
28
|
+
request(service: ServiceFilter, req: Message): Promise<Message>;
|
|
29
|
+
requestTo(endpointId: string | null, service: ServiceFilter, req: Message): Promise<Message>;
|
|
30
|
+
advertise(service: ServiceFilter, handler: ServiceHandler): void;
|
|
31
|
+
unadvertise(serviceName: string): void;
|
|
32
|
+
setServiceHandler(serviceName: string, handler: ServiceHandler): void;
|
|
33
|
+
publish(topic: string, text: string): void;
|
|
34
|
+
subscribe(topic: string, handler: (text: string) => void): void;
|
|
35
|
+
unsubscribe(topic: string): void;
|
|
36
|
+
isConnected(): boolean;
|
|
37
|
+
addConnectListener(listener: Function): void;
|
|
34
38
|
}
|
|
39
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,184 +1,185 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
function messageFromString(text) {
|
|
2
|
+
const index = text.indexOf('\n');
|
|
3
|
+
if (index == -1) {
|
|
4
|
+
return {
|
|
5
|
+
header: JSON.parse(text)
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
return {
|
|
10
|
+
header: JSON.parse(text.slice(0, index)),
|
|
11
|
+
payload: text.slice(index + 1)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ServiceBroker {
|
|
16
|
+
constructor(url) {
|
|
17
|
+
this.url = url;
|
|
18
|
+
this.providers = new Map();
|
|
19
|
+
this.ws = null;
|
|
20
|
+
this.connectListeners = [];
|
|
21
|
+
this.pendingSend = [];
|
|
22
|
+
this.pendingResponses = new Map();
|
|
23
|
+
this.pendingIdGen = 0;
|
|
24
|
+
this.connect();
|
|
25
|
+
}
|
|
26
|
+
connect() {
|
|
27
|
+
const conn = new WebSocket(this.url);
|
|
28
|
+
conn.onopen = () => this.onOpen(conn);
|
|
29
|
+
conn.onerror = () => {
|
|
30
|
+
console.error("Failed to connect to service broker, retrying in 15");
|
|
31
|
+
setTimeout(() => this.connect(), 15000);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
onOpen(conn) {
|
|
35
|
+
this.ws = conn;
|
|
36
|
+
this.ws.onerror = console.error;
|
|
37
|
+
this.ws.onclose = () => this.onClose();
|
|
38
|
+
this.ws.onmessage = event => this.onMessage(event);
|
|
39
|
+
for (const listener of this.connectListeners)
|
|
40
|
+
listener();
|
|
41
|
+
for (const { header, payload } of this.pendingSend)
|
|
42
|
+
this.send(header, payload);
|
|
43
|
+
this.pendingSend = [];
|
|
44
|
+
}
|
|
45
|
+
onClose() {
|
|
46
|
+
this.ws = null;
|
|
47
|
+
console.error("Lost connection to service broker, reconnecting");
|
|
48
|
+
setTimeout(() => this.connect(), 0);
|
|
49
|
+
}
|
|
50
|
+
onMessage(e) {
|
|
51
|
+
const msg = messageFromString(e.data);
|
|
52
|
+
console.debug("<<", msg.header, msg.payload);
|
|
53
|
+
if (msg.header.type == "ServiceResponse")
|
|
54
|
+
this.onServiceResponse(msg);
|
|
55
|
+
else if (msg.header.type == "ServiceRequest")
|
|
56
|
+
this.onServiceRequest(msg);
|
|
57
|
+
else if (msg.header.type == "SbStatusResponse")
|
|
58
|
+
this.onServiceResponse(msg);
|
|
59
|
+
else if (msg.header.error)
|
|
60
|
+
this.onServiceResponse(msg);
|
|
61
|
+
else
|
|
62
|
+
console.error("Unhandled", msg.header);
|
|
63
|
+
}
|
|
64
|
+
onServiceResponse(message) {
|
|
65
|
+
const id = message.header.id;
|
|
66
|
+
const pendingResponse = this.pendingResponses.get(id);
|
|
67
|
+
if (pendingResponse) {
|
|
68
|
+
this.pendingResponses.delete(id);
|
|
69
|
+
if (message.header.error) {
|
|
70
|
+
pendingResponse.reject(new Error(message.header.error));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
pendingResponse.fulfill(message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.error("Response received but no pending request", message.header);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
onServiceRequest(msg) {
|
|
81
|
+
const service = msg.header.service;
|
|
82
|
+
const provider = this.providers.get(service.name);
|
|
83
|
+
if (provider) {
|
|
84
|
+
Promise.resolve(provider.handler(msg))
|
|
85
|
+
.then(res => {
|
|
86
|
+
if (msg.header.id) {
|
|
87
|
+
this.send(Object.assign(Object.assign({}, res === null || res === void 0 ? void 0 : res.header), { to: msg.header.from, id: msg.header.id, type: "ServiceResponse" }), res === null || res === void 0 ? void 0 : res.payload);
|
|
88
|
+
}
|
|
83
89
|
})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
text)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.addConnectListener = function(listener) {
|
|
181
|
-
connectListeners.push(listener);
|
|
182
|
-
if (this.isConnected()) listener();
|
|
183
|
-
}
|
|
90
|
+
.catch(err => {
|
|
91
|
+
if (msg.header.id) {
|
|
92
|
+
this.send({
|
|
93
|
+
to: msg.header.from,
|
|
94
|
+
id: msg.header.id,
|
|
95
|
+
type: "ServiceResponse",
|
|
96
|
+
error: err.message || err
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error(err.message, msg.header);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.error("No handler for service " + service.name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
send(header, payload) {
|
|
109
|
+
if (!this.ws) {
|
|
110
|
+
this.pendingSend.push({ header, payload });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.debug(">>", header, payload);
|
|
114
|
+
if (payload) {
|
|
115
|
+
this.ws.send(JSON.stringify(header) + "\n" + payload);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.ws.send(JSON.stringify(header));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
request(service, req) {
|
|
122
|
+
return this.requestTo(null, service, req);
|
|
123
|
+
}
|
|
124
|
+
requestTo(endpointId, service, req) {
|
|
125
|
+
const id = ++this.pendingIdGen;
|
|
126
|
+
const promise = new Promise((fulfill, reject) => {
|
|
127
|
+
this.pendingResponses.set(id, { fulfill, reject });
|
|
128
|
+
});
|
|
129
|
+
const header = {
|
|
130
|
+
id: id,
|
|
131
|
+
type: "ServiceRequest",
|
|
132
|
+
service
|
|
133
|
+
};
|
|
134
|
+
if (endpointId)
|
|
135
|
+
header.to = endpointId;
|
|
136
|
+
this.send(Object.assign(Object.assign({}, req.header), header), req.payload);
|
|
137
|
+
return promise;
|
|
138
|
+
}
|
|
139
|
+
advertise(service, handler) {
|
|
140
|
+
if (this.providers.has(service.name)) {
|
|
141
|
+
throw new Error(service.name + " provider already exists");
|
|
142
|
+
}
|
|
143
|
+
this.providers.set(service.name, { advertisedService: service, handler });
|
|
144
|
+
return this.send({
|
|
145
|
+
type: "SbAdvertiseRequest",
|
|
146
|
+
services: Array.from(this.providers.values())
|
|
147
|
+
.filter(x => x.advertisedService)
|
|
148
|
+
.map(x => x.advertisedService)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
unadvertise(serviceName) {
|
|
152
|
+
if (!this.providers.delete(serviceName)) {
|
|
153
|
+
throw new Error(serviceName + " provider not exists");
|
|
154
|
+
}
|
|
155
|
+
return this.send({
|
|
156
|
+
type: "SbAdvertiseRequest",
|
|
157
|
+
services: Array.from(this.providers.values())
|
|
158
|
+
.filter(x => x.advertisedService)
|
|
159
|
+
.map(x => x.advertisedService)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
setServiceHandler(serviceName, handler) {
|
|
163
|
+
if (this.providers.has(serviceName)) {
|
|
164
|
+
throw new Error("Handler already exists");
|
|
165
|
+
}
|
|
166
|
+
this.providers.set(serviceName, { handler });
|
|
167
|
+
}
|
|
168
|
+
publish(topic, text) {
|
|
169
|
+
return this.send({ type: "ServiceRequest", service: { name: "#" + topic } }, text);
|
|
170
|
+
}
|
|
171
|
+
subscribe(topic, handler) {
|
|
172
|
+
return this.advertise({ name: "#" + topic }, msg => handler(msg.payload));
|
|
173
|
+
}
|
|
174
|
+
unsubscribe(topic) {
|
|
175
|
+
return this.unadvertise("#" + topic);
|
|
176
|
+
}
|
|
177
|
+
isConnected() {
|
|
178
|
+
return this.ws != null;
|
|
179
|
+
}
|
|
180
|
+
addConnectListener(listener) {
|
|
181
|
+
this.connectListeners.push(listener);
|
|
182
|
+
if (this.isConnected())
|
|
183
|
+
listener();
|
|
184
|
+
}
|
|
184
185
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@service-broker/webclient",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Browser
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Browser ESM client library for communicating with the service broker",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/service-broker/service-broker-webclient.git"
|
|
@@ -13,5 +13,8 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/service-broker/service-broker-webclient/issues"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.com/service-broker/service-broker-webclient#readme"
|
|
16
|
+
"homepage": "https://github.com/service-broker/service-broker-webclient#readme",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.8.3"
|
|
19
|
+
}
|
|
17
20
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
|
|
2
|
+
interface ServiceFilter {
|
|
3
|
+
name: string
|
|
4
|
+
capabilities?: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ServiceHandler {
|
|
8
|
+
(request: Message): Message | void | Promise<Message | void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface MessageWithHeader {
|
|
12
|
+
header: Record<string, unknown>
|
|
13
|
+
payload?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Message {
|
|
17
|
+
header?: Record<string, unknown>
|
|
18
|
+
payload?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingResponse {
|
|
22
|
+
fulfill(response: Message): void
|
|
23
|
+
reject(err: unknown): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function messageFromString(text: string): MessageWithHeader {
|
|
27
|
+
const index = text.indexOf('\n')
|
|
28
|
+
if (index == -1) {
|
|
29
|
+
return {
|
|
30
|
+
header: JSON.parse(text)
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
return {
|
|
34
|
+
header: JSON.parse(text.slice(0, index)),
|
|
35
|
+
payload: text.slice(index + 1)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export class ServiceBroker {
|
|
42
|
+
|
|
43
|
+
private readonly providers = new Map<string, {
|
|
44
|
+
advertisedService?: ServiceFilter
|
|
45
|
+
handler: ServiceHandler
|
|
46
|
+
}>()
|
|
47
|
+
private ws: WebSocket | null = null
|
|
48
|
+
private readonly connectListeners: Function[] = []
|
|
49
|
+
private pendingSend: Message[] = []
|
|
50
|
+
private readonly pendingResponses = new Map<number, PendingResponse>()
|
|
51
|
+
private pendingIdGen = 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
constructor(private readonly url: string) {
|
|
55
|
+
this.connect()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private connect() {
|
|
59
|
+
const conn = new WebSocket(this.url)
|
|
60
|
+
conn.onopen = () => this.onOpen(conn)
|
|
61
|
+
conn.onerror = () => {
|
|
62
|
+
console.error("Failed to connect to service broker, retrying in 15")
|
|
63
|
+
setTimeout(() => this.connect(), 15000)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private onOpen(conn: WebSocket) {
|
|
68
|
+
this.ws = conn
|
|
69
|
+
this.ws.onerror = console.error
|
|
70
|
+
this.ws.onclose = () => this.onClose()
|
|
71
|
+
this.ws.onmessage = event => this.onMessage(event)
|
|
72
|
+
for (const listener of this.connectListeners) listener()
|
|
73
|
+
for (const { header, payload } of this.pendingSend) this.send(header, payload)
|
|
74
|
+
this.pendingSend = []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private onClose() {
|
|
78
|
+
this.ws = null
|
|
79
|
+
console.error("Lost connection to service broker, reconnecting")
|
|
80
|
+
setTimeout(() => this.connect(), 0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private onMessage(e: MessageEvent) {
|
|
84
|
+
const msg = messageFromString(e.data);
|
|
85
|
+
console.debug("<<", msg.header, msg.payload);
|
|
86
|
+
if (msg.header.type == "ServiceResponse") this.onServiceResponse(msg)
|
|
87
|
+
else if (msg.header.type == "ServiceRequest") this.onServiceRequest(msg)
|
|
88
|
+
else if (msg.header.type == "SbStatusResponse") this.onServiceResponse(msg)
|
|
89
|
+
else if (msg.header.error) this.onServiceResponse(msg)
|
|
90
|
+
else console.error("Unhandled", msg.header)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private onServiceResponse(message: MessageWithHeader) {
|
|
94
|
+
const id = message.header.id as number
|
|
95
|
+
const pendingResponse = this.pendingResponses.get(id)
|
|
96
|
+
if (pendingResponse) {
|
|
97
|
+
this.pendingResponses.delete(id)
|
|
98
|
+
if (message.header.error) {
|
|
99
|
+
pendingResponse.reject(new Error(message.header.error as string))
|
|
100
|
+
} else {
|
|
101
|
+
pendingResponse.fulfill(message)
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
console.error("Response received but no pending request", message.header)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private onServiceRequest(msg: MessageWithHeader) {
|
|
109
|
+
const service = msg.header.service as ServiceFilter
|
|
110
|
+
const provider = this.providers.get(service.name)
|
|
111
|
+
if (provider) {
|
|
112
|
+
Promise.resolve(provider.handler(msg))
|
|
113
|
+
.then(res => {
|
|
114
|
+
if (msg.header.id) {
|
|
115
|
+
this.send({
|
|
116
|
+
...res?.header,
|
|
117
|
+
to: msg.header.from,
|
|
118
|
+
id: msg.header.id,
|
|
119
|
+
type: "ServiceResponse"
|
|
120
|
+
}, res?.payload)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch(err => {
|
|
124
|
+
if (msg.header.id) {
|
|
125
|
+
this.send({
|
|
126
|
+
to: msg.header.from,
|
|
127
|
+
id: msg.header.id,
|
|
128
|
+
type: "ServiceResponse",
|
|
129
|
+
error: err.message || err
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
console.error(err.message, msg.header)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
} else {
|
|
136
|
+
console.error("No handler for service " + service.name)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private send(header: Message['header'], payload?: Message['payload']) {
|
|
141
|
+
if (!this.ws) {
|
|
142
|
+
this.pendingSend.push({ header, payload })
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.debug(">>", header, payload);
|
|
146
|
+
if (payload) {
|
|
147
|
+
this.ws.send(JSON.stringify(header) + "\n" + payload)
|
|
148
|
+
} else {
|
|
149
|
+
this.ws.send(JSON.stringify(header))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
request(service: ServiceFilter, req: Message) {
|
|
155
|
+
return this.requestTo(null, service, req);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
requestTo(endpointId: string | null, service: ServiceFilter, req: Message) {
|
|
159
|
+
const id = ++this.pendingIdGen
|
|
160
|
+
const promise = new Promise<Message>((fulfill, reject) => {
|
|
161
|
+
this.pendingResponses.set(id, { fulfill, reject })
|
|
162
|
+
})
|
|
163
|
+
const header: Message['header'] = {
|
|
164
|
+
id: id,
|
|
165
|
+
type: "ServiceRequest",
|
|
166
|
+
service
|
|
167
|
+
};
|
|
168
|
+
if (endpointId) header.to = endpointId;
|
|
169
|
+
this.send({...req.header, ...header}, req.payload)
|
|
170
|
+
return promise;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
advertise(service: ServiceFilter, handler: ServiceHandler) {
|
|
174
|
+
if (this.providers.has(service.name)) {
|
|
175
|
+
throw new Error(service.name + " provider already exists")
|
|
176
|
+
}
|
|
177
|
+
this.providers.set(service.name, { advertisedService: service, handler })
|
|
178
|
+
return this.send({
|
|
179
|
+
type: "SbAdvertiseRequest",
|
|
180
|
+
services: Array.from(this.providers.values())
|
|
181
|
+
.filter(x => x.advertisedService)
|
|
182
|
+
.map(x => x.advertisedService)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
unadvertise(serviceName: string) {
|
|
187
|
+
if (!this.providers.delete(serviceName)) {
|
|
188
|
+
throw new Error(serviceName + " provider not exists")
|
|
189
|
+
}
|
|
190
|
+
return this.send({
|
|
191
|
+
type: "SbAdvertiseRequest",
|
|
192
|
+
services: Array.from(this.providers.values())
|
|
193
|
+
.filter(x => x.advertisedService)
|
|
194
|
+
.map(x => x.advertisedService)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setServiceHandler(serviceName: string, handler: ServiceHandler) {
|
|
199
|
+
if (this.providers.has(serviceName)) {
|
|
200
|
+
throw new Error("Handler already exists")
|
|
201
|
+
}
|
|
202
|
+
this.providers.set(serviceName, { handler })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
publish(topic: string, text: string) {
|
|
206
|
+
return this.send({ type: "ServiceRequest", service: { name: "#" + topic } }, text)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
subscribe(topic: string, handler: (text: string) => void) {
|
|
210
|
+
return this.advertise({ name: "#" + topic }, msg => handler(msg.payload!))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
unsubscribe(topic: string) {
|
|
214
|
+
return this.unadvertise("#" + topic)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
isConnected() {
|
|
218
|
+
return this.ws != null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
addConnectListener(listener: Function) {
|
|
222
|
+
this.connectListeners.push(listener)
|
|
223
|
+
if (this.isConnected()) listener()
|
|
224
|
+
}
|
|
225
|
+
}
|
package/tsconfig.json
ADDED