{"record":{"id":"923280f3889ff241","repo":"ruvnet/ruflo","slug":"localtransport-closed","errorCode":null,"errorMessage":"LocalTransport: closed","messagePattern":"LocalTransport: closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/swarm/src/consensus/transport.ts","lineNumber":257,"sourceCode":"    if (target.keyPair && target.resolvePeerPublicKey) {\n      const pub = target.resolvePeerPublicKey(msg.from);\n      if (!pub || !verifyMessage(msg, pub)) {\n        throw new Error(`LocalTransport: signature verification failed for message from ${msg.from}`);\n      }\n      // Replay defense: seq must be strictly increasing per sender.\n      if (typeof msg.seq === 'number') {\n        const last = target.lastSeenSeq.get(msg.from) ?? 0;\n        if (msg.seq <= last) throw new Error(`LocalTransport: replayed/out-of-order seq from ${msg.from} (${msg.seq} <= ${last})`);\n        target.lastSeenSeq.set(msg.from, msg.seq);\n      }\n    }\n    if (!target.handler) return null;\n    const reply = await target.handler(msg);\n    return (reply ?? null) as ConsensusReply;\n  }\n\n  async send(to: string, msg: Omit<ConsensusMessage, 'from'>, timeoutMs?: number): Promise<ConsensusReply> {\n    if (this.closed) throw new Error('LocalTransport: closed');\n    const target = this.registry.get(to);\n    if (!target) throw new Error(`LocalTransport: unreachable peer ${to}`);\n    const stamped = this.stamp(msg);\n    const t = timeoutMs ?? this.defaultTimeoutMs;\n    return Promise.race([\n      this.deliver(target, stamped),\n      new Promise<ConsensusReply>((_, rej) => setTimeout(() => rej(new Error(`LocalTransport: send to ${to} timed out (${t}ms)`)), t)),\n    ]);\n  }\n\n  async broadcast(msg: Omit<ConsensusMessage, 'from'>): Promise<void> {\n    if (this.closed) throw new Error('LocalTransport: closed');\n    const stamped = this.stamp(msg);\n    await Promise.allSettled(\n      this.registry.peerIds(this.nodeId).map(id => {\n        const target = this.registry.get(id);\n        return target ? this.deliver(target, stamped).catch(() => {}) : Promise.resolve();\n      }),","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/swarm/src/consensus/transport.ts#L239-L275","documentation":"send() checks its own closed flag first and rejects if close() was already called on this transport. close() also unregisters the node from the registry, so the transport is permanently unusable afterwards — this is a use-after-close lifecycle error, not a transient failure.","triggerScenarios":"Any await transport.send(...) that runs after await transport.close() — typically a consensus timer (election timeout, heartbeat) or an application promise that was not stopped/drained before teardown A race where close() wins against a send() scheduled in the same tick (close sets closed synchronously; send checks it before delivering) Tests that close in afterEach but let pending protocol loops continue into the next test","commonSituations":"Forgetting to stop the raft/byzantine/gossip protocol instance before closing its transport Shared/singleton transports closed by one code path while another still sends Promise chains surviving component unmount in long-running services","solutions":["Stop the consensus protocol (timers/loops) before calling transport.close()","After close, never reuse the instance — create a new LocalTransport (and re-register it) instead","Guard sends with an isClosed check or route all sends through one owner that owns the lifecycle","In tests, fully await shutdown in afterEach before the next case starts"],"exampleFix":"// before\nawait consensus.stop();\nsomeTimer = setInterval(() => transport.send(leader, heartbeatMsg), 50); // still firing\nawait transport.close(); // interval's next send throws 'LocalTransport: closed'\n\n// after\nawait consensus.stop();\nclearInterval(someTimer); // stop all senders first\nawait transport.close();","handlingStrategy":"type-guard","validationCode":"// Owner-of-lifecycle pattern: only send while not torn down\nlet closed = false;\nasync function safeSend(t, to, msg) {\n  if (closed) return null;\n  return t.send(to, msg);\n}\n// close path:\nclosed = true; await drain(); await t.close();","typeGuard":"// Wrap the transport in a guard object (closed flag is private on LocalTransport):\nfunction isUsable(t, registry) { return registry.get(t.nodeId) === t; } // close() unregisters\nif (!isUsable(tx, registry)) throw new Error('transport already closed');","tryCatchPattern":"try { await transport.send(to, msg); }\ncatch (e) {\n  if (e instanceof Error && e.message === 'LocalTransport: closed') return; // expected during teardown\n  throw e;\n}","preventionTips":["Cancel every interval/loop that can call send() BEFORE close()","Own the transport in exactly one component; close it there once, after senders stop","Never reuse a closed LocalTransport — construct a new one"],"tags":["consensus","transport","lifecycle","use-after-close","typescript"],"backgroundTag":"use-after-close","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}