UNPKG

@interopio/gateway-server

Version:

## `@glue42/gateway-ent` compatibility

102 lines (90 loc) 4.01 kB
import getLogger from '../logger.js'; const logger = getLogger('mesh.connections'); export type Node = { node: string, endpoint: string, users?: string[] }; export type NodeConnection = { node: string, connect: Node[] } type NodeValue = Omit<Node, 'users'> & { users: Set<string>, memberId: number, lastAccess: number } export interface NodeConnections { announce(nodes: Node[]): NodeConnection[]; remove(node: string): boolean; } export default class InMemoryNodeConnections implements NodeConnections { private readonly nodes = new Map<string, NodeValue>(); private readonly nodesByEndpoint = new Map<string, string>(); private memberIds: number = 0; constructor(private readonly timeout: number = 60000) { } announce(nodes: Node[]): NodeConnection[] { for (const node of nodes) { const {node: nodeId, users, endpoint} = node; const foundId = this.nodesByEndpoint.get(endpoint); if (foundId) { if (foundId !== nodeId) { logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`); this.nodesByEndpoint.set(endpoint, nodeId); this.nodes.delete(foundId); } } else { logger.info(`endpoint ${endpoint} announced for ${nodeId}`); this.nodesByEndpoint.set(endpoint, nodeId); } this.nodes.set(nodeId, this.updateNode(node, new Set<string>(users ?? []), nodeId, this.nodes.get(nodeId))); } this.cleanupOldNodes(); const sortedNodes = Array.from(this.nodes.values()).sort((a, b) => a.memberId - b.memberId); return nodes.map((e) => { const node = e.node; const connect = this.findConnections(sortedNodes, this.nodes.get(node)); return {node, connect}; }); } remove(nodeId: string) { const removed = this.nodes.get(nodeId); if (removed) { this.nodes.delete(nodeId); const endpoint = removed.endpoint; this.nodesByEndpoint.delete(endpoint); logger.info(`endpoint ${endpoint} removed for ${nodeId}`); return true; } return false; } private updateNode(newNode: Node, users: Set<string>, _key: string, oldNode?: NodeValue): NodeValue { const node: Omit<NodeValue, 'lastAccess' | 'users'> = !oldNode ? {...newNode, memberId: this.memberIds++} : oldNode; return {...node, users, lastAccess: Date.now()}; } private cleanupOldNodes() { const threshold = Date.now() - this.timeout; for (const [nodeId,v] of this.nodes) { if (v.lastAccess < threshold) { if (logger.enabledFor('debug')) { logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.timeout} ms.`); } this.nodes.delete(nodeId); } } } private findConnections(sortedNodes: NodeValue[], node?: NodeValue): Node[] { return sortedNodes.reduce((l, c) => { if (node !== undefined && c.memberId < node.memberId) { const intersection = new Set(c.users); node.users.forEach(user => { if (!c.users.has(user)) { intersection.delete(user); } }); c.users.forEach(user => { if (!node.users.has(user)) { intersection.delete(user); } }); if (intersection.size > 0) { const e: Node = {node: c.node, endpoint: c.endpoint}; return l.concat(e); } } return l; }, new Array<Node>()); } }