-
Notifications
You must be signed in to change notification settings - Fork 6
feat(core): coordinator holds the hub connection (gateway role) #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
28dda31
feat(core): add CoordinatorGateway connection-lifecycle collaborator
Mearman 5064b6d
test(core): extract real-hub-over-ws test harness into hub-helpers
Mearman 3e6ef17
feat(core): add connectHub/disconnectHub to the transport contract
Mearman 2ab8e2e
feat(core): wire CoordinatorGateway into MeshStore's become/lose paths
Mearman 2fefaef
feat(core): thread hubUrl through createBridgeMesh/createBridgeMeshSync
Mearman e253b4f
fix(core): isolate a hub dial failure from local coordinator election
Mearman 35ef7fe
test(core): point the multi-process smoke test at an unreachable hub
Mearman cb67f5d
fix(core): drop state_sync/state_update relayed by hub peers
Mearman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * CoordinatorGateway — attaches the cross-machine gateway role to this machine's local coordinator (agent-comms#154, agent-comms#153's first leg). A bridge that becomes the mesh's local coordinator, whether by a fresh bind (MeshStore's own init()) or a takeover (PeerLifecycle's own handleBecomeCoordinator), also becomes the machine's gateway: it dials the hub and holds the connection for as long as it holds the coordinator role. Losing the role, gracefully or by crash, drops the connection; the next coordinator re-dials as part of taking over. Hub-side state is therefore rebuilt from scratch on every takeover -- messages in flight during the gap are lost, the same loss class as a coordinator crash today, now on the data path. Forwarding local agents onto the hub and merging its directory back (agent-comms#155) is deliberately not this class's concern; it owns only the connection lifecycle. | ||
| */ | ||
|
|
||
| export interface CoordinatorGatewayDeps { | ||
| /** The hub URL this machine's gateway dials -- configuration, defaulting to DEFAULT_HUB_URL (mesh-store-shared.ts). */ | ||
| hubUrl: string; | ||
| /** Dials the hub. Backed by the transport's own optional connectHub -- a transport with no gateway capability is never asked to redial by anything else in this class. */ | ||
| connectHub: (url: string) => Promise<void>; | ||
| /** Drops the held hub connection, if any. Backed by the transport's own optional disconnectHub. */ | ||
| disconnectHub: () => Promise<void>; | ||
| /** Reports a hub-dial failure. Never invoked for anything else -- onBecameCoordinator's own guarantee (see its doc comment) is that a hub problem is always reported this way, never thrown, so this is the only signal a caller gets that the gateway role didn't actually connect. */ | ||
| onError?: (error: Error) => void; | ||
| } | ||
|
|
||
| export class CoordinatorGateway { | ||
| private connected = false; | ||
|
|
||
| constructor(private readonly deps: Readonly<CoordinatorGatewayDeps>) {} | ||
|
|
||
| /** Whether this side currently holds the gateway role: it has dialled the hub and has not since lost coordinator status. */ | ||
| get isConnected(): boolean { | ||
| return this.connected; | ||
| } | ||
|
|
||
| /** Dials the hub for this machine's gateway role. Idempotent: a call while already connected is a no-op, since nothing in this codebase's own coordinator-election machinery re-fires "became coordinator" without an intervening onLostCoordinator -- this guard is defensive, not a known double-fire path. Never throws: local coordinator election (the whole reason this side is calling this at all) must not depend on the hub being reachable, so a dial failure is reported via deps.onError and swallowed here, leaving isConnected false so a later onBecameCoordinator call retries rather than being blocked by the earlier failure's own idempotency guard. */ | ||
| async onBecameCoordinator(): Promise<void> { | ||
| if (this.connected) return; | ||
| try { | ||
| await this.deps.connectHub(this.deps.hubUrl); | ||
| this.connected = true; | ||
| } catch (error) { | ||
| this.deps.onError?.( | ||
| error instanceof Error ? error : new Error(String(error)), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** Drops this machine's held hub connection. A no-op if this side never became the gateway, or already lost the role -- MeshStore.shutdown() calls this unconditionally regardless of coordinator status, so this guard is what makes that safe rather than a redundant extra close. */ | ||
| async onLostCoordinator(): Promise<void> { | ||
| if (!this.connected) return; | ||
| this.connected = false; | ||
| await this.deps.disconnectHub(); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** | ||
| * mergeKnownDevices — the mesh-wide gossip-directory aggregation WireMeshTransport's own listKnownDevices reads from. Split out purely to keep wire-mesh-transport.ts under the repo's max-lines cap, the same reason connection-approval.ts, room-router.ts, hub-session.ts, and peer-lifecycle.ts were each split from their own owning file. | ||
| */ | ||
|
|
||
| import { deviceIdToHex } from "wire-mesh-core/domain/device-id"; | ||
| import type { DirectoryEntry } from "wire-mesh-core/domain/mesh-session"; | ||
| import type { PeerAdvert } from "wire-mesh-core/generated/protocol"; | ||
|
|
||
| /** Merges one session event's own directory into the mesh-wide knownDevices view (mutated in place), keeping the newer advert (by snapshot-seconds) whenever a device-id is already known from an earlier event or a different session. */ | ||
| export function mergeKnownDevices( | ||
| knownDevices: Map<string, PeerAdvert>, | ||
| directory: readonly DirectoryEntry[], | ||
| ): void { | ||
| for (const entry of directory) { | ||
| const deviceIdHex = deviceIdToHex(entry.device); | ||
| const existing = knownDevices.get(deviceIdHex); | ||
| if ( | ||
| existing === undefined || | ||
| entry.advert["snapshot-seconds"] >= existing["snapshot-seconds"] | ||
| ) { | ||
| knownDevices.set(deviceIdHex, entry.advert); | ||
| } | ||
| } | ||
| } |
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.