-
Notifications
You must be signed in to change notification settings - Fork 99
Expand file tree
/
Copy pathservient.ts
More file actions
252 lines (217 loc) · 9.52 KB
/
Copy pathservient.ts
File metadata and controls
252 lines (217 loc) · 9.52 KB
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
83
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/********************************************************************************
* Copyright (c) 2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/
import * as WoT from "wot-typescript-definitions";
import WoTImpl from "./wot-impl";
import ExposedThing from "./exposed-thing";
import { ProtocolClientFactory, ProtocolServer, ProtocolClient } from "./protocol-interfaces";
import ContentManager, { ContentCodec } from "./content-serdes";
const uuid = require("uuid");
import { createLoggers } from "./logger";
import { Helpers, Thing } from "./core";
const { debug, warn } = createLoggers("core", "servient");
export default class Servient {
private servers: Array<ProtocolServer> = [];
private clientFactories: Map<string, ProtocolClientFactory> = new Map<string, ProtocolClientFactory>();
private things: Map<string, ExposedThing> = new Map<string, ExposedThing>();
private credentialStore: Map<string, Array<unknown>> = new Map<string, Array<unknown>>();
/**
* Data schema mapping for extracting values from nested response objects.
* @experimental
*/
public dataSchemaMapping: Thing["nw:dataSchemaMapping"];
#wotInstance?: typeof WoT;
#shutdown = false;
/** add a new codec to support a mediatype; offered mediatypes are listed in TDs */
public addMediaType(codec: ContentCodec, offered = false): void {
ContentManager.addCodec(codec, offered);
}
public expose(thing: ExposedThing): Promise<void> {
if (this.servers.length === 0) {
warn(`Servient has no servers to expose Things`);
return new Promise<void>((resolve) => {
resolve();
});
}
debug(`Servient exposing '${thing.title}'`);
// What is a good way to to convey forms information like contentType et cetera for interactions
const tdTemplate: WoT.ThingDescription = Helpers.structuredClone(thing) as WoT.ThingDescription;
// initializing forms fields
thing.forms = [];
for (const property of Object.values(thing.properties)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
property.forms = [];
}
for (const action of Object.values(thing.actions)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
action.forms = [];
}
for (const event of Object.values(thing.events)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
event.forms = [];
}
const serverPromises: Promise<void>[] = [];
this.servers.forEach((server) => {
serverPromises.push(server.expose(thing, tdTemplate));
});
return new Promise<void>((resolve, reject) => {
Promise.all(serverPromises)
.then(() => resolve())
.catch((err) => reject(err));
});
}
public addThing(thing: ExposedThing): boolean {
if (!thing.id) {
thing.id = "urn:uuid:" + uuid.v4();
warn(`Servient generating ID for '${thing.title}': '${thing.id}'`);
}
if (!this.things.has(thing.id)) {
this.things.set(thing.id, thing);
debug(`Servient reset ID '${thing.id}' with '${thing.title}'`);
return true;
} else {
return false;
}
}
public destroyThing(thingId: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
if (this.things.has(thingId)) {
debug(`Servient destroying thing with id '${thingId}'`);
this.things.delete(thingId);
const serverPromises: Promise<boolean>[] = [];
this.servers.forEach((server) => {
serverPromises.push(server.destroy(thingId));
});
Promise.all(serverPromises)
.then(() => resolve(true))
.catch((err) => reject(err));
} else {
warn(`Servient was asked to destroy thing but failed to find thing with id '${thingId}'`);
resolve(false);
}
});
}
public getThing(id: string): ExposedThing | undefined {
if (this.things.has(id)) {
return this.things.get(id);
} else return undefined;
}
// FIXME should be getThingDescriptions (breaking change)
public getThings(): Record<string, WoT.ThingDescription> {
debug(`Servient getThings size == '${this.things.size}'`);
const ts: { [key: string]: WoT.ThingDescription } = {};
this.things.forEach((thing, id) => {
ts[id] = thing.getThingDescription();
});
return ts;
}
public addServer(server: ProtocolServer): boolean {
// add all exposed Things to new server
this.things.forEach((thing, id) => server.expose(thing));
this.servers.push(server);
return true;
}
public getServers(): Array<ProtocolServer> {
// return a copy -- FIXME: not a deep copy
return this.servers.slice(0);
}
public addClientFactory(clientFactory: ProtocolClientFactory): void {
debug(`Servient adding client factory for '${clientFactory.scheme}'`);
this.clientFactories.set(clientFactory.scheme, clientFactory);
}
public removeClientFactory(scheme: string): boolean {
debug(`Servient removing client factory for '${scheme}'`);
this.clientFactories.get(scheme)?.destroy();
return this.clientFactories.delete(scheme);
}
public hasClientFor(scheme: string): boolean {
debug(`Servient checking for '${scheme}' scheme in ${this.clientFactories.size} ClientFactories`);
return this.clientFactories.has(scheme);
}
public getClientFor(scheme: string): ProtocolClient {
const clientFactory = this.clientFactories.get(scheme);
if (clientFactory) {
debug(`Servient creating client for scheme '${scheme}'`);
return clientFactory.getClient();
} else {
// FIXME returning null was bad - Error or Promise?
// h0ru5: caller cannot react gracefully - I'd throw Error
throw new Error(`Servient has no ClientFactory for scheme '${scheme}'`);
}
}
public getClientSchemes(): string[] {
return Array.from(this.clientFactories.keys());
}
public addCredentials(credentials: Record<string, unknown>): void {
for (const [credentialKey, credentialValue] of Object.entries(credentials ?? {})) {
debug(`Servient storing credentials for '${credentialKey}'`);
const currentCredentials = this.credentialStore.get(credentialKey) ?? [];
if (currentCredentials.length === 0) {
this.credentialStore.set(credentialKey, currentCredentials);
}
currentCredentials.push(credentialValue);
}
}
/**
* @deprecated use retrieveCredentials() instead which may return multiple credentials
*
* @param identifier id
*/
public getCredentials(identifier: string): unknown {
debug(`Servient looking up credentials for '${identifier}' (@deprecated)`);
const currentCredentials = this.credentialStore.get(identifier);
if (currentCredentials && currentCredentials.length > 0) {
// return first
return currentCredentials[0];
} else {
return undefined;
}
}
public retrieveCredentials(identifier: string): Array<unknown> | undefined {
debug(`Servient looking up credentials for '${identifier}'`);
return this.credentialStore.get(identifier);
}
// will return WoT object
public async start(): Promise<typeof WoT> {
if (this.#wotInstance !== undefined) {
debug("Servient started already -> nop -> returning previous WoT implementation");
return this.#wotInstance;
}
if (this.#shutdown) {
throw Error("Servient cannot be started (again) since it was already stopped");
}
const serverStatus: Array<Promise<void>> = [];
this.servers.forEach((server) => serverStatus.push(server.start(this)));
this.clientFactories.forEach((clientFactory) => clientFactory.init());
await Promise.all(serverStatus);
return (this.#wotInstance = new WoTImpl(this));
}
public async shutdown(): Promise<void> {
if (this.#wotInstance === undefined) {
throw Error("Servient cannot be shutdown, wasn't even started");
}
if (this.#shutdown) {
debug("Servient shutdown already -> nop");
return;
}
this.clientFactories.forEach((clientFactory) => clientFactory.destroy());
const promises = this.servers.map((server) => server.stop());
await Promise.all(promises);
this.#shutdown = true;
this.#wotInstance = undefined; // clean-up reference
}
}