Created
November 13, 2025 21:57
-
-
Save efortner/f273c05756e70d3ba490a00c4b67d5b5 to your computer and use it in GitHub Desktop.
Network Simulator (v1)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| - Network of nodes === devices | |
| - Devices can join the network by going to an entry node and asking to join | |
| - Devices can communicate to all other devices once within the network via message | |
| - Devices can drop out of the network, return, then see messages that were given to them while they were gone | |
| - Messages are raw bytes sent between devices | |
| Non-functional requirements: | |
| - Up to 1000 devices | |
| type NodeId = string; | |
| interface Node { | |
| id: string; | |
| primaryNetworkNode: PrimaryNode; | |
| } | |
| interface PrimaryNode { | |
| connectedNodes: Node[]; | |
| messageQueues: Map<NodeId, Messages[]>; | |
| requestToJoin: (node: Node) => Promise<boolean>; | |
| drop: (node: Node) => Promise<boolean>; | |
| isNetworkMember: (node: Node) => Promise<boolean>; | |
| } | |
| interface Message { | |
| byteBuffer: Buffer; | |
| } | |
| */ | |
| const MAX_NODES = 1000; | |
| interface Message { | |
| id: string; | |
| byteBuffer: Buffer; | |
| } | |
| type NodeId = string; | |
| interface NodeProps { | |
| readonly id: NodeId; | |
| readonly primaryNode: PrimaryNode; | |
| readonly totalSimulatedActions: number; | |
| } | |
| export class Node { | |
| private _remainingSimulatedActions: number; | |
| constructor(private readonly props: NodeProps) { | |
| this._remainingSimulatedActions = props.totalSimulatedActions; | |
| } | |
| public get id() { | |
| return this.props.id; | |
| } | |
| public get primaryNode() { | |
| return this.props.primaryNode; | |
| } | |
| public get remainingSimulatedActions() { | |
| return this._remainingSimulatedActions; | |
| } | |
| public readonly receiveMessage = (message: Message) => { | |
| console.info(`${this.id} received message ${message.id}`); | |
| }; | |
| public readonly run = async (): Promise<void> => { | |
| if (this.remainingSimulatedActions === 0) { | |
| throw new Error(`${this.id} has no remaining actions`); | |
| } | |
| const allAvailableActions = [ | |
| this.sendMessageAction, | |
| this.runJoinAction, | |
| this.runDropAction, | |
| ]; | |
| const nextAction = | |
| allAvailableActions[ | |
| Math.trunc(Math.random() * allAvailableActions.length) | |
| ]!; | |
| await nextAction(); | |
| this._remainingSimulatedActions -= 1; | |
| }; | |
| private readonly sendMessageAction = async (): Promise<void> => { | |
| const { allNodes } = this.props.primaryNode; | |
| const recipient = | |
| allNodes[Math.trunc(Math.random() * MAX_NODES) % allNodes.length]!; | |
| await this.props.primaryNode.sendMessage(this, recipient, { | |
| id: `Message-${this.id}-To-${recipient.id}-${this.remainingSimulatedActions}`, | |
| byteBuffer: Buffer.from(`Hello, ${recipient.id}`), | |
| }); | |
| }; | |
| private readonly runJoinAction = async (): Promise<void> => { | |
| await this.props.primaryNode.requestToJoin(this); | |
| }; | |
| private readonly runDropAction = async (): Promise<void> => { | |
| await this.props.primaryNode.drop(this); | |
| }; | |
| } | |
| export class PrimaryNode { | |
| private readonly _connectedNodes: Set<NodeId> = new Set(); | |
| private readonly _messageQueues: Map<NodeId, Message[]> = new Map(); | |
| private readonly _allNodes: Node[] = []; | |
| constructor() {} | |
| public readonly addNodes = (nodes: Node[]): void => { | |
| this._allNodes.push(...nodes); | |
| }; | |
| public readonly requestToJoin = async (node: Node): Promise<boolean> => { | |
| if (!this._connectedNodes.has(node.id)) { | |
| this._connectedNodes.add(node.id); | |
| console.info(`${node.id} was added to the network`); | |
| return true; | |
| } | |
| console.info(`${node.id} could not be added to the network`); | |
| return false; | |
| }; | |
| public readonly drop = async (node: Node): Promise<boolean> => { | |
| if (this._connectedNodes.has(node.id)) { | |
| this._connectedNodes.delete(node.id); | |
| console.info(`${node.id} dropped from the network`); | |
| return true; | |
| } | |
| console.info(`${node.id} could not be dropped from the network`); | |
| return false; | |
| }; | |
| public readonly isNetworkMember = async (node: Node): Promise<boolean> => { | |
| return this._connectedNodes.has(node.id); | |
| }; | |
| public readonly getConnectedNodes = async (): Promise<Node[]> => { | |
| return [...this._allNodes.filter((node) => this.isNetworkMember(node))]; | |
| }; | |
| public readonly deliverMessages = async (): Promise<void> => { | |
| this.getConnectedNodes().then((connectedNodes) => | |
| connectedNodes.map((node) => { | |
| const nodeMessageQueue = this._messageQueues.get(node.id); | |
| if (nodeMessageQueue !== undefined) { | |
| nodeMessageQueue.forEach((message) => node.receiveMessage(message)); | |
| this._messageQueues.set(node.id, []); | |
| } | |
| }), | |
| ); | |
| }; | |
| public get allNodes() { | |
| return [...this._allNodes]; | |
| } | |
| public readonly sendMessage = async ( | |
| sender: Node, | |
| recipient: Node, | |
| message: Message, | |
| ): Promise<void> => { | |
| if (!this._connectedNodes.has(sender.id)) { | |
| console.info( | |
| `${sender.id} attempted to send a message but is not part of this network`, | |
| ); | |
| return; | |
| } | |
| const nodeMessageQueue = this._messageQueues.get(recipient.id); | |
| if (nodeMessageQueue !== undefined) { | |
| nodeMessageQueue.push(message); | |
| } else { | |
| this._messageQueues.set(recipient.id, [message]); | |
| } | |
| console.info(`${sender.id} sent ${message.id} to ${recipient.id}`); | |
| }; | |
| } | |
| (async () => { | |
| const TOTAL_NODES = 10; | |
| const MAX_NODE_ACTIONS = 10; | |
| const primaryNode = new PrimaryNode(); | |
| const nodes: Node[] = Array.from({ length: TOTAL_NODES }).map( | |
| (_, index) => | |
| new Node({ | |
| id: `Node-${index}`, | |
| totalSimulatedActions: Math.trunc(Math.random() * MAX_NODE_ACTIONS), | |
| primaryNode, | |
| }), | |
| ); | |
| primaryNode.addNodes(nodes); | |
| const getTurnsRemaining = () => | |
| primaryNode.allNodes | |
| .map((node) => node.remainingSimulatedActions) | |
| .reduce((first, second) => { | |
| if (first > second) { | |
| return first; | |
| } | |
| return second; | |
| }); | |
| while (getTurnsRemaining() > 0) { | |
| await Promise.all( | |
| primaryNode.allNodes | |
| .filter((node) => node.remainingSimulatedActions > 0) | |
| .map((node) => node.run()), | |
| ); | |
| await primaryNode.deliverMessages(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment