@interopio/gateway-server
Version:
## `@glue42/gateway-ent` compatibility
102 lines (90 loc) • 4.01 kB
text/typescript
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>());
}
}