github.com/simpleiot/simpleiot@v0.18.3/frontend/lib/siot-nats.mjs (about) 1 import { connect as natsConnect, StringCodec } from "nats.ws" 2 3 // The syntax: `import { Timestamp } from ...` will not work properly 4 // in Node.js because of how protobuf generates the CommonJS code, so we 5 // have to do a little more work... 6 import timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb.js" 7 const { Timestamp } = timestamp_pb 8 import point_pb from "./protobuf/point_pb.js" 9 const { Points, Point } = point_pb 10 import node_pb from "./protobuf/node_pb.js" 11 const { NodesRequest } = node_pb 12 import message_pb from "./protobuf/message_pb.js" 13 const { Message } = message_pb 14 import notification_pb from "./protobuf/notification_pb.js" 15 const { Notification } = notification_pb 16 17 // eslint-disable-next-line new-cap 18 const strCodec = StringCodec() 19 20 // connect opens and returns a connection to SIOT / NATS via WebSockets 21 export * from "nats.ws" 22 export async function connect(opts = {}) { 23 const { servers = ["ws://localhost:9222"] } = opts 24 const nc = await natsConnect({ ...opts, servers }) 25 26 // Force SIOTConnection to inherit from `nc` prototype 27 SIOTConnection.prototype = Object.create( 28 Object.getPrototypeOf(nc), 29 Object.getOwnPropertyDescriptors(SIOTConnection.prototype) 30 ) 31 // Create new instance of SIOTConnection and then assign `nc` properties 32 return Object.assign(new SIOTConnection(), nc) 33 } 34 35 // SIOTConnection is a wrapper around a NatsConnectionImpl 36 function SIOTConnection() { 37 // do nothing 38 } 39 40 Object.assign(SIOTConnection.prototype, { 41 // getNode sends a request to `nodes.<parent>.<id>` to retrieve an array of 42 // NodeEdges for the specified Node id. 43 // - If `id` is "all" or falsy, this calls `getNodeChildren` instead (see 44 // below); however, we strongly recommend using `getNodeChildren` directly 45 // - If `parent` is "all" or falsy, all instances of the specified node are 46 // returned 47 // - If `parent` is "root", only root nodes are returned 48 // - `opts` are options passed to the NATS request 49 async getNode(id, { parent, type, includeDel, opts } = {}) { 50 if (id === "all" || id === "*" || !id) { 51 return this.getNodeChildren(parent, { type, includeDel, opts }) 52 } 53 if (parent === "*") { 54 parent = "all" 55 } 56 57 const points = [ 58 { type: "nodeType", text: type }, 59 { type: "tombstone", value: includeDel ? 1 : 0 }, 60 ] 61 const payload = encodePoints(points, this.userID) 62 const m = await this.request( 63 "nodes." + (parent || "all") + "." + id, 64 payload, 65 opts 66 ) 67 return decodeNodesRequest(m.data) 68 }, 69 70 // getNodeChildren sends a request to `nodes.<parentID>.<id>` to retrieve 71 // an array of child NodeEdges of the specified parent node. 72 // - If `parent` is "root", all root nodes are retrieved 73 // - `type` - can be used to filter nodes of a specified type (defaults to "") 74 // - `includeDel` - set to true to include deleted nodes (defaults to false) 75 // - `recursive` - set to true to recursively retrieve all descendants matching 76 // the criteria. In this case, each returned NodeEdge will contain a 77 // `children` property, which is an array of that Node's descendant NodeEdges. 78 // 79 // Set to "flat" to return a single flattened array of NodeEdges. 80 // 81 // Note: If `type` is also set when `recursive` is truthy, `type` still 82 // restricts which nodes are recursively searched. Consider removing the 83 // `type` filter and filter manually. 84 // - `opts` are options passed to the NATS request 85 async getNodeChildren( 86 parentID, 87 { type, includeDel, recursive, opts, _cache = {} } = {} 88 ) { 89 if ( 90 parentID === "all" || 91 parentID === "*" || 92 parentID === "none" || 93 !parentID 94 ) { 95 throw new Error("parent node ID must be specified") 96 } 97 98 const points = [ 99 { type: "nodeType", text: type }, 100 { type: "tombstone", value: includeDel ? 1 : 0 }, 101 ] 102 103 const payload = encodePoints(points, this.userID) 104 const m = await this.request("nodes." + parentID + ".all", payload, opts) 105 const nodeEdges = decodeNodesRequest(m.data) 106 if (!recursive) { 107 return nodeEdges 108 } 109 110 const flat = recursive === "flat" 111 const nodeChildren = [] // used if flattening 112 // Note: recursive calls are done serially to fully utilize 113 // the temporary `_cache` 114 for (const n of nodeEdges) { 115 const children = 116 _cache[n.id] || 117 (await this.getNodeChildren(n.id, { 118 type, 119 includeDel, 120 recursive, 121 opts, 122 _cache, 123 })) 124 // update cache 125 // eslint-disable-next-line require-atomic-updates 126 _cache[n.id] = children 127 if (flat) { 128 nodeChildren.push(...children) 129 } else { 130 // If not flattening, add `children` key to `n` 131 n.children = children 132 } 133 } 134 if (flat) { 135 // If flattening, simply return flat array of node edges 136 return nodeEdges.concat(nodeChildren) 137 } 138 return nodeEdges 139 }, 140 141 // getNodesForUser returns the parent nodes for the given userID along 142 // with their descendants if `recursive` is truthy. 143 // - `type` - can be used to filter nodes of a specified type (defaults to "") 144 // - `includeDel` - set to true to include deleted nodes (defaults to false) 145 // - `recursive` - set to true to recursively retrieve all descendants matching 146 // the criteria. In this case, each returned NodeEdge will contain a 147 // `children` property, which is an array of that Node's descendant NodeEdges. 148 // Set to "flat" to return a single flattened array of NodeEdges. 149 // - `opts` are options passed to the NATS request 150 async getNodesForUser(userID, { type, includeDel, recursive, opts } = {}) { 151 // Get root nodes of the user node 152 const rootNodes = await this.getNode(userID, { 153 parent: "all", 154 includeDel, 155 opts, 156 }) 157 158 // Create function to filter nodes based on `type` and `includeDel` 159 const filterFunc = (n) => { 160 const tombstone = n.edgepointsList.find((e) => e.type === "tombstone") 161 return ( 162 (!type || n.type === type) && 163 (includeDel || (tombstone && tombstone.value % 2 === 0)) 164 ) 165 } 166 167 const _cache = {} 168 const flat = recursive === "flat" 169 const parentNodes = await Promise.all( 170 rootNodes.filter(filterFunc).map(async (n) => { 171 const [parentNode] = await this.getNode(n.parent, { opts }) 172 if (!recursive) { 173 return parentNode 174 } 175 const children = await this.getNodeChildren(n.parent, { 176 // TODO: Not sure if `type` should be passed here since we need 177 // to do recursive search 178 type, 179 includeDel, 180 recursive, 181 opts, 182 _cache, 183 }) 184 if (flat) { 185 return [parentNode].concat(children) 186 } 187 return Object.assign(parentNode, { children }) 188 }) 189 ) 190 if (flat) { 191 return parentNodes.flat() 192 } 193 return parentNodes 194 }, 195 196 // _subscribePointsSubject subscribes to a NATS subject and returns an async 197 // iterable for Point objects 198 _subscribePointsSubject(subject) { 199 const [subjectType] = subject.split(".") 200 const sub = this.subscribe(subject) 201 // Return subscription wrapped by new async iterator 202 return Object.assign(Object.create(sub), { 203 async *[Symbol.asyncIterator]() { 204 // Iterator reads and decodes array of Points from subscription 205 for await (const m of sub) { 206 const { pointsList } = Points.deserializeBinary(m.data).toObject() 207 if (pointsList.length === 0) { 208 // Just abort in the rare case that no points are 209 // emitted, but a NATS message was published anyway 210 continue 211 } 212 // Convert `time` to JavaScript date and return each point 213 for (const p of pointsList) { 214 p.time = new Date(p.time.seconds * 1e3 + p.time.nanos / 1e6) 215 if (!p.key) { 216 p.key = "0" 217 } 218 } 219 // Send points as a array of points 220 if (subjectType === "up") { 221 const [, upstreamID, nodeID, parentID] = m.subject.split(".") 222 yield { 223 upstreamID, 224 nodeID, 225 parentID, // populated for edge points 226 subject: m.subject, 227 points: pointsList, 228 } 229 } else { 230 const [, nodeID, parentID] = m.subject.split(".") 231 yield { 232 nodeID, 233 parentID, // populated for edge points 234 subject: m.subject, 235 points: pointsList, 236 } 237 } 238 } 239 }, 240 }) 241 }, 242 243 // Subscribes to `p.<nodeID>` and returns an async iterable for an array of 244 // Point objects. `nodeID` can be `*` or `all`. 245 subscribePoints(nodeID) { 246 if (nodeID === "all") { 247 nodeID = "*" 248 } 249 return this._subscribePointsSubject("p." + nodeID) 250 }, 251 // Subscribes to `p.<nodeID>.<parentID>` and returns an async iterable for 252 // an array of Point objects. `nodeID` or `parentID` can be `*` or `all`. 253 subscribeEdgePoints(nodeID, parentID) { 254 if (nodeID === "all") { 255 nodeID = "*" 256 } 257 if (parentID === "all") { 258 parentID = "*" 259 } 260 return this._subscribePointsSubject("p." + nodeID + "." + parentID) 261 }, 262 // Subscribes to `up.<upstreamID>.<nodeID>` and returns an async iterable 263 // for an array of Point objects. `nodeID` can be `*` or `all`. 264 subscribeUpstreamPoints(upstreamID, nodeID) { 265 if (nodeID === "all") { 266 nodeID = "*" 267 } 268 return this._subscribePointsSubject("up." + upstreamID + "." + nodeID) 269 }, 270 // Subscribes to `up.<upstreamID>.<nodeID>.<parentID>` and returns an async 271 // iterable for an array of Point objects. `nodeID` or `parentID` can be 272 // `*` or `all`. 273 subscribeUpstreamEdgePoints(upstreamID, nodeID, parentID) { 274 if (nodeID === "all") { 275 nodeID = "*" 276 } 277 if (parentID === "all") { 278 parentID = "*" 279 } 280 return this._subscribePointsSubject( 281 "up." + upstreamID + "." + nodeID + "." + parentID 282 ) 283 }, 284 285 // setUserID sets the user ID for this connection; any points / edge points 286 // sent from this connection will have their origin set to the specified 287 // userID 288 setUserID(userID) { 289 this.userID = userID 290 }, 291 292 // sendNodePoints sends an array of `points` for a given `nodeID` 293 // - `ack` - true if function should block waiting for send acknowledgement 294 // (defaults to true) 295 // - `opts` are options passed to the NATS request 296 async sendNodePoints(nodeID, points, { ack = true, opts } = {}) { 297 const payload = encodePoints(points, this.userID) 298 if (!ack) { 299 await this.publish("p." + nodeID, payload, opts) 300 return 301 } 302 303 const m = await this.request("p." + nodeID, payload, opts) 304 305 // Assume message data is an error message 306 if (m.data && m.data.length > 0) { 307 throw new Error( 308 `error sending points for node '${nodeID}': ` + strCodec.decode(m.data) 309 ) 310 } 311 }, 312 313 // sendEdgePoints sends an array of `edgePoints` for the edge between 314 // `nodeID` and `parentID` 315 // - `ack` - true if function should block waiting for send acknowledgement 316 // (defaults to true) 317 // - `opts` are options passed to the NATS request 318 async sendEdgePoints( 319 nodeID, 320 parentID, 321 edgePoints, 322 { ack = true, opts } = {} 323 ) { 324 const payload = encodePoints(edgePoints, this.userID) 325 if (!ack) { 326 await this.publish("p." + nodeID + "." + parentID, payload, opts) 327 return 328 } 329 330 const m = await this.request("p." + nodeID + "." + parentID, payload, opts) 331 332 // Assume message data is an error message 333 if (m.data && m.data.length > 0) { 334 throw new Error( 335 `error sending edge points between nodes '${nodeID}' and '${parentID}': ` + 336 strCodec.decode(m.data) 337 ) 338 } 339 }, 340 341 // subscribeMessages subscribes to `node.<nodeID>.msg` and returns an async 342 // iterable for Message objects 343 subscribeMessages(nodeID) { 344 const sub = this.subscribe("node." + nodeID + ".msg") 345 // Return subscription wrapped by new async iterator 346 return Object.assign(Object.create(sub), { 347 async *[Symbol.asyncIterator]() { 348 // Iterator reads and decodes Messages from subscription 349 for await (const m of sub) { 350 yield Message.deserializeBinary(m.data).toObject() 351 } 352 }, 353 }) 354 }, 355 356 // subscribeNotifications subscribes to `node.<nodeID>.not` and returns an async 357 // iterable for Notification objects 358 subscribeNotifications(nodeID) { 359 const sub = this.subscribe("node." + nodeID + ".not") 360 // Return subscription wrapped by new async iterator 361 return Object.assign(Object.create(sub), { 362 async *[Symbol.asyncIterator]() { 363 // Iterator reads and decodes Messages from subscription 364 for await (const m of sub) { 365 yield Notification.deserializeBinary(m.data).toObject() 366 } 367 }, 368 }) 369 }, 370 }) 371 372 // decodeNodesRequest decodes a protobuf-encoded NodesRequest and returns 373 // the array of nodes returned by the request 374 function decodeNodesRequest(data) { 375 const { nodesList, error } = NodesRequest.deserializeBinary(data).toObject() 376 if (error) { 377 throw new Error("NodesRequest decode error: " + error) 378 } 379 380 for (const n of nodesList) { 381 // Convert `time` to JavaScript date for each point 382 for (const p of n.pointsList) { 383 p.time = new Date(p.time.seconds * 1e3 + p.time.nanos / 1e6) 384 } 385 for (const p of n.edgepointsList) { 386 p.time = new Date(p.time.seconds * 1e3 + p.time.nanos / 1e6) 387 } 388 } 389 return nodesList 390 } 391 392 // encodePoints returns protobuf encoded Points 393 function encodePoints(points, userID) { 394 const payload = new Points() 395 // Convert `time` from JavaScript date if needed 396 points = points.map((p) => { 397 if (p instanceof Point) { 398 return p 399 } 400 let { time = new Date() } = p 401 const { type, value, text, key, tombstone, data, origin } = p 402 p = new Point() 403 if (!(time instanceof Timestamp)) { 404 let { seconds, nanos } = time 405 if (time instanceof Date) { 406 const ms = time.valueOf() 407 seconds = Math.round(ms / 1e3) 408 nanos = (ms % 1e3) * 1e6 409 } 410 time = new Timestamp() 411 time.setSeconds(seconds) 412 time.setNanos(nanos) 413 } 414 p.setType(type) 415 if (value || value === 0) { 416 p.setValue(value) 417 } 418 p.setTime(time) 419 if (text) { 420 p.setText(text) 421 } 422 p.setKey(key || "0") 423 if (tombstone) { 424 p.setTombstone(tombstone) 425 } 426 if (data) { 427 p.setData(data) 428 } 429 if (userID || origin) { 430 // Note: Prefer `userID` over point `origin` 431 p.setOrigin(userID || origin) 432 } 433 return p 434 }) 435 payload.setPointsList(points) 436 return payload.serializeBinary() 437 }