@service-broker/webclient 2.0.0 → 2.0.2

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 CHANGED
@@ -1,15 +1,111 @@
1
- # service-broker-webclient
2
- Browser JavaScript library for communicating with the service broker
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
- ## Usage
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", console)
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.request({name: "my-service"}, {payload: "my-request"})
14
- .then(({payload}) => handleResult(payload))
108
+ sb.unsubscribe(
109
+ topic: string
110
+ ): Promise<void>
15
111
  ```
package/dist/index.d.ts CHANGED
@@ -1,34 +1,45 @@
1
-
2
- interface ServiceFilter {
3
- name: string
4
- capabilities?: string[]
1
+ export interface ServiceSelector {
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
- (msg: Message): Partial<Message>|void|Promise<Partial<Message>|void>
6
+ (request: MessageWithHeader): Message | void | Promise<Message | void>;
7
+ }
8
+ type MessageHeader = Record<string, unknown>;
9
+ type MessagePayload = string;
10
+ export interface MessageWithHeader {
11
+ header: MessageHeader;
12
+ payload?: MessagePayload;
15
13
  }
16
-
17
- interface Message {
18
- header: any
19
- payload: any
14
+ export interface Message {
15
+ header?: MessageHeader;
16
+ payload?: MessagePayload;
20
17
  }
21
-
22
18
  export declare class ServiceBroker {
23
- constructor(url: string, logger: {error: Console["error"], debug: Console["debug"]});
24
- request(service: ServiceFilter, req: Partial<Message>): Promise<Message>;
25
- requestTo(endpointId: string, service: ServiceFilter, req: Partial<Message>): Promise<Message>;
26
- advertise(service: ServiceAdvert, handler: ServiceHandler): Promise<void>;
27
- unadvertise(serviceName: string): Promise<void>;
28
- setHandler(serviceName: string, handler: ServiceHandler): void;
29
- publish(topic: string, text: string): Promise<void>;
30
- subscribe(topic: string, handler: (text: string) => void): Promise<void>;
31
- unsubscribe(topic: string): Promise<void>;
32
- isConnected(): boolean;
33
- addConnectListener(listener: () => void): void;
19
+ private readonly url;
20
+ private readonly providers;
21
+ private ws;
22
+ private readonly connectListeners;
23
+ private pendingSend;
24
+ private readonly pendingResponses;
25
+ private pendingIdGen;
26
+ constructor(url: string);
27
+ private connect;
28
+ private onOpen;
29
+ private onClose;
30
+ private onMessage;
31
+ private onServiceResponse;
32
+ private onServiceRequest;
33
+ private send;
34
+ request(service: ServiceSelector, req: Message): Promise<Message>;
35
+ requestTo(endpointId: string | null, service: ServiceSelector, req: Message): Promise<Message>;
36
+ advertise(service: ServiceSelector, handler: ServiceHandler): void;
37
+ unadvertise(serviceName: string): void;
38
+ setServiceHandler(serviceName: string, handler: ServiceHandler): void;
39
+ publish(topic: string, text: string): void;
40
+ subscribe(topic: string, handler: (text: string) => void): void;
41
+ unsubscribe(topic: string): void;
42
+ isConnected(): boolean;
43
+ addConnectListener(listener: Function): void;
34
44
  }
45
+ export {};
package/dist/index.js CHANGED
@@ -1,182 +1,185 @@
1
- export function ServiceBroker(url, logger) {
2
- var pending = {};
3
- var pendingIdGen = 0;
4
- var providers = {};
5
- var ws;
6
- var connectListeners = [];
7
- var pendingSend = [];
8
- connect();
9
-
10
- function connect() {
11
- var conn = new WebSocket(url);
12
- conn.onopen = onOpen.bind(null, conn);
13
- conn.onerror = function() {
14
- logger.error("Failed to connect to service broker, retrying in 15");
15
- setTimeout(connect, 15000);
16
- };
17
- }
18
-
19
- function onOpen(conn) {
20
- ws = conn;
21
- ws.onerror = logger.error;
22
- ws.onclose = onClose;
23
- ws.onmessage = onMessage;
24
- for (var i=0; i<connectListeners.length; i++) connectListeners[i]();
25
- for (var i=0; i<pendingSend.length; i++) send(pendingSend[i].header, pendingSend[i].payload);
26
- pendingSend = [];
27
- }
28
-
29
- function onClose() {
30
- ws = null;
31
- logger.error("Lost connection to service broker, reconnecting");
32
- setTimeout(connect, 0);
33
- }
34
-
35
- function onMessage(e) {
36
- var msg = messageFromString(e.data);
37
- logger.debug("<<", msg.header, msg.payload);
38
- if (msg.header.type == "ServiceResponse") onServiceResponse(msg);
39
- else if (msg.header.type == "ServiceRequest") onServiceRequest(msg);
40
- else if (msg.header.type == "SbStatusResponse") onServiceResponse(msg);
41
- else if (msg.header.error) onServiceResponse(msg);
42
- else logger.error("Unhandled", msg.header);
43
- }
44
-
45
- function messageFromString(text) {
46
- var index = text.indexOf('\n');
47
- if (index == -1) return {header: JSON.parse(text)};
48
- else return {header: JSON.parse(text.substr(0,index)), payload: text.substr(index+1)};
49
- }
50
-
51
- function onServiceResponse(msg) {
52
- if (pending[msg.header.id]) {
53
- if (msg.header.error) pending[msg.header.id].reject(new Error(msg.header.error));
54
- else pending[msg.header.id].fulfill(msg);
55
- delete pending[msg.header.id];
56
- }
57
- else logger.error("Response received but no pending request", msg.header);
58
- }
59
-
60
- function onServiceRequest(msg) {
61
- if (providers[msg.header.service.name]) {
62
- Promise.resolve(providers[msg.header.service.name].handler(msg))
63
- .then(function(res) {
64
- if (!res) res = {};
65
- if (msg.header.id) {
66
- var header = {
67
- to: msg.header.from,
68
- id: msg.header.id,
69
- type: "ServiceResponse"
70
- };
71
- send(Object.assign({}, res.header, header), res.payload);
72
- }
73
- })
74
- .catch(function(err) {
75
- if (msg.header.id) {
76
- send({
77
- to: msg.header.from,
78
- id: msg.header.id,
79
- type: "ServiceResponse",
80
- error: err.message
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
+ }
81
89
  })
82
- }
83
- else logger.error(err.message, msg.header);
84
- })
85
- }
86
- else logger.error("No handler for service " + msg.header.service.name);
87
- }
88
-
89
-
90
-
91
- this.request = function(service, req) {
92
- return this.requestTo(null, service, req);
93
- }
94
-
95
- this.requestTo = function(endpointId, service, req) {
96
- var id = ++pendingIdGen;
97
- var promise = new Promise(function(fulfill, reject) {
98
- pending[id] = {fulfill: fulfill, reject: reject};
99
- })
100
- var header = {
101
- id: id,
102
- type: "ServiceRequest",
103
- service: service
104
- };
105
- if (endpointId) header.to = endpointId;
106
- send(Object.assign({}, req.header, header), req.payload);
107
- return promise;
108
- }
109
-
110
- function send(header, payload) {
111
- if (!ws) {
112
- pendingSend.push({header: header, payload: payload});
113
- return;
114
- }
115
- logger.debug(">>", header, payload);
116
- ws.send(JSON.stringify(header) + (payload ? "\n"+payload : ""));
117
- }
118
-
119
-
120
-
121
- this.advertise = function(service, handler) {
122
- if (providers[service.name]) throw new Error(service.name + " provider already exists");
123
- providers[service.name] = {
124
- advertisedService: service,
125
- handler: handler
126
- }
127
- return send({
128
- type: "SbAdvertiseRequest",
129
- services: Object.keys(providers)
130
- .map(function(x) {return providers[x].advertisedService})
131
- .filter(function(x) {return x})
132
- })
133
- }
134
-
135
- this.unadvertise = function(serviceName) {
136
- if (!providers[serviceName]) throw new Error(serviceName + " provider not exists");
137
- delete providers[serviceName];
138
- return send({
139
- type: "SbAdvertiseRequest",
140
- services: Object.keys(providers)
141
- .map(function(x) {return providers[x].advertisedService})
142
- .filter(function(x) {return x})
143
- })
144
- }
145
-
146
- this.setHandler = function(serviceName, handler) {
147
- if (providers[serviceName]) throw new Error("Handler already exists");
148
- providers[serviceName] = {
149
- handler: handler
150
- }
151
- }
152
-
153
-
154
-
155
- this.publish = function(topic, text) {
156
- return send({
157
- type: "ServiceRequest",
158
- service: {name: "#"+topic}
159
- },
160
- text);
161
- }
162
-
163
- this.subscribe = function(topic, handler) {
164
- return this.advertise({name: "#"+topic}, function(msg) {
165
- handler(msg.payload);
166
- return null;
167
- })
168
- }
169
-
170
- this.unsubscribe = function(topic) {
171
- return this.unadvertise("#"+topic);
172
- }
173
-
174
- this.isConnected = function() {
175
- return ws != null;
176
- }
177
-
178
- this.addConnectListener = function(listener) {
179
- connectListeners.push(listener);
180
- if (this.isConnected()) listener();
181
- }
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
+ }
182
185
  }
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@service-broker/webclient",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Browser ESM client library for communicating with the service broker",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
7
  "repository": {
9
8
  "type": "git",
10
- "url": "git+https://github.com/service-broker/service-broker-webclient.git"
9
+ "url": "git+https://github.com/service-broker/webclient.git"
11
10
  },
12
11
  "author": "Hai Phan <hai.phan@gmail.com>",
13
12
  "license": "MIT",
14
13
  "bugs": {
15
- "url": "https://github.com/service-broker/service-broker-webclient/issues"
14
+ "url": "https://github.com/service-broker/webclient/issues"
16
15
  },
17
- "homepage": "https://github.com/service-broker/service-broker-webclient#readme"
16
+ "homepage": "https://github.com/service-broker/webclient#readme",
17
+ "devDependencies": {
18
+ "typescript": "^5.8.3"
19
+ }
18
20
  }
package/src/index.ts ADDED
@@ -0,0 +1,228 @@
1
+
2
+ export interface ServiceSelector {
3
+ name: string
4
+ capabilities?: string[]
5
+ }
6
+
7
+ export interface ServiceHandler {
8
+ (request: MessageWithHeader): Message | void | Promise<Message | void>
9
+ }
10
+
11
+ type MessageHeader = Record<string, unknown>
12
+ type MessagePayload = string
13
+
14
+ export interface MessageWithHeader {
15
+ header: MessageHeader
16
+ payload?: MessagePayload
17
+ }
18
+
19
+ export interface Message {
20
+ header?: MessageHeader
21
+ payload?: MessagePayload
22
+ }
23
+
24
+ interface PendingResponse {
25
+ fulfill(response: Message): void
26
+ reject(err: unknown): void
27
+ }
28
+
29
+ function messageFromString(text: string): MessageWithHeader {
30
+ const index = text.indexOf('\n')
31
+ if (index == -1) {
32
+ return {
33
+ header: JSON.parse(text)
34
+ }
35
+ } else {
36
+ return {
37
+ header: JSON.parse(text.slice(0, index)),
38
+ payload: text.slice(index + 1)
39
+ }
40
+ }
41
+ }
42
+
43
+
44
+ export class ServiceBroker {
45
+
46
+ private readonly providers = new Map<string, {
47
+ advertisedService?: ServiceSelector
48
+ handler: ServiceHandler
49
+ }>()
50
+ private ws: WebSocket | null = null
51
+ private readonly connectListeners: Function[] = []
52
+ private pendingSend: MessageWithHeader[] = []
53
+ private readonly pendingResponses = new Map<number, PendingResponse>()
54
+ private pendingIdGen = 0
55
+
56
+
57
+ constructor(private readonly url: string) {
58
+ this.connect()
59
+ }
60
+
61
+ private connect() {
62
+ const conn = new WebSocket(this.url)
63
+ conn.onopen = () => this.onOpen(conn)
64
+ conn.onerror = () => {
65
+ console.error("Failed to connect to service broker, retrying in 15")
66
+ setTimeout(() => this.connect(), 15000)
67
+ }
68
+ }
69
+
70
+ private onOpen(conn: WebSocket) {
71
+ this.ws = conn
72
+ this.ws.onerror = console.error
73
+ this.ws.onclose = () => this.onClose()
74
+ this.ws.onmessage = event => this.onMessage(event)
75
+ for (const listener of this.connectListeners) listener()
76
+ for (const { header, payload } of this.pendingSend) this.send(header, payload)
77
+ this.pendingSend = []
78
+ }
79
+
80
+ private onClose() {
81
+ this.ws = null
82
+ console.error("Lost connection to service broker, reconnecting")
83
+ setTimeout(() => this.connect(), 0)
84
+ }
85
+
86
+ private onMessage(e: MessageEvent) {
87
+ const msg = messageFromString(e.data);
88
+ console.debug("<<", msg.header, msg.payload);
89
+ if (msg.header.type == "ServiceResponse") this.onServiceResponse(msg)
90
+ else if (msg.header.type == "ServiceRequest") this.onServiceRequest(msg)
91
+ else if (msg.header.type == "SbStatusResponse") this.onServiceResponse(msg)
92
+ else if (msg.header.error) this.onServiceResponse(msg)
93
+ else console.error("Unhandled", msg.header)
94
+ }
95
+
96
+ private onServiceResponse(message: MessageWithHeader) {
97
+ const id = message.header.id as number
98
+ const pendingResponse = this.pendingResponses.get(id)
99
+ if (pendingResponse) {
100
+ this.pendingResponses.delete(id)
101
+ if (message.header.error) {
102
+ pendingResponse.reject(new Error(message.header.error as string))
103
+ } else {
104
+ pendingResponse.fulfill(message)
105
+ }
106
+ } else {
107
+ console.error("Response received but no pending request", message.header)
108
+ }
109
+ }
110
+
111
+ private onServiceRequest(msg: MessageWithHeader) {
112
+ const service = msg.header.service as ServiceSelector
113
+ const provider = this.providers.get(service.name)
114
+ if (provider) {
115
+ Promise.resolve(provider.handler(msg))
116
+ .then(res => {
117
+ if (msg.header.id) {
118
+ this.send({
119
+ ...res?.header,
120
+ to: msg.header.from,
121
+ id: msg.header.id,
122
+ type: "ServiceResponse"
123
+ }, res?.payload)
124
+ }
125
+ })
126
+ .catch(err => {
127
+ if (msg.header.id) {
128
+ this.send({
129
+ to: msg.header.from,
130
+ id: msg.header.id,
131
+ type: "ServiceResponse",
132
+ error: err.message || err
133
+ })
134
+ } else {
135
+ console.error(err.message, msg.header)
136
+ }
137
+ })
138
+ } else {
139
+ console.error("No handler for service " + service.name)
140
+ }
141
+ }
142
+
143
+ private send(header: MessageHeader, payload?: MessagePayload) {
144
+ if (!this.ws) {
145
+ this.pendingSend.push({ header, payload })
146
+ return;
147
+ }
148
+ console.debug(">>", header, payload);
149
+ if (payload) {
150
+ this.ws.send(JSON.stringify(header) + "\n" + payload)
151
+ } else {
152
+ this.ws.send(JSON.stringify(header))
153
+ }
154
+ }
155
+
156
+
157
+ request(service: ServiceSelector, req: Message) {
158
+ return this.requestTo(null, service, req);
159
+ }
160
+
161
+ requestTo(endpointId: string | null, service: ServiceSelector, req: Message) {
162
+ const id = ++this.pendingIdGen
163
+ const promise = new Promise<Message>((fulfill, reject) => {
164
+ this.pendingResponses.set(id, { fulfill, reject })
165
+ })
166
+ const header: MessageHeader = {
167
+ id: id,
168
+ type: "ServiceRequest",
169
+ service
170
+ };
171
+ if (endpointId) header.to = endpointId;
172
+ this.send({...req.header, ...header}, req.payload)
173
+ return promise;
174
+ }
175
+
176
+ advertise(service: ServiceSelector, handler: ServiceHandler) {
177
+ if (this.providers.has(service.name)) {
178
+ throw new Error(service.name + " provider already exists")
179
+ }
180
+ this.providers.set(service.name, { advertisedService: service, handler })
181
+ return this.send({
182
+ type: "SbAdvertiseRequest",
183
+ services: Array.from(this.providers.values())
184
+ .filter(x => x.advertisedService)
185
+ .map(x => x.advertisedService)
186
+ })
187
+ }
188
+
189
+ unadvertise(serviceName: string) {
190
+ if (!this.providers.delete(serviceName)) {
191
+ throw new Error(serviceName + " provider not exists")
192
+ }
193
+ return this.send({
194
+ type: "SbAdvertiseRequest",
195
+ services: Array.from(this.providers.values())
196
+ .filter(x => x.advertisedService)
197
+ .map(x => x.advertisedService)
198
+ })
199
+ }
200
+
201
+ setServiceHandler(serviceName: string, handler: ServiceHandler) {
202
+ if (this.providers.has(serviceName)) {
203
+ throw new Error("Handler already exists")
204
+ }
205
+ this.providers.set(serviceName, { handler })
206
+ }
207
+
208
+ publish(topic: string, text: string) {
209
+ return this.send({ type: "ServiceRequest", service: { name: "#" + topic } }, text)
210
+ }
211
+
212
+ subscribe(topic: string, handler: (text: string) => void) {
213
+ return this.advertise({ name: "#" + topic }, msg => handler(msg.payload!))
214
+ }
215
+
216
+ unsubscribe(topic: string) {
217
+ return this.unadvertise("#" + topic)
218
+ }
219
+
220
+ isConnected() {
221
+ return this.ws != null
222
+ }
223
+
224
+ addConnectListener(listener: Function) {
225
+ this.connectListeners.push(listener)
226
+ if (this.isConnected()) listener()
227
+ }
228
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "esnext",
4
+ "target": "es2017",
5
+ "strict": true,
6
+ "moduleResolution": "bundler",
7
+ "declaration": true,
8
+ "newLine": "LF",
9
+ "rootDir": "src",
10
+ "outDir": "dist"
11
+ },
12
+ "include": [
13
+ "src/**/*"
14
+ ]
15
+ }