export class ConnectionManager {
/**
* Connections and their per-agent delivery projection share one Ref so
* authentication, disconnect cleanup, and subscription updates are atomic.
* This prevents an old last-disconnect cleanup from deleting a newly
* authenticated socket's freshly hydrated subscriptions.
*/
private readonly stateRef: Ref.Ref<ConnectionManagerState> = Effect.runSync(
Ref.make({
connections: HashMap.empty<ConnectionId, Connection>(),
agentConversationSubscriptions: HashMap.empty<
AgentId,
HashSet.HashSet<ConversationId>
>(),
}),
);
/**
* Insert a fresh `UnauthenticatedConnection`. Called by the socket handler
* at WebSocket open. The Connect handler promotes it to the agent arm.
* @param connId Value supplied to the operation.
* @param socket Value supplied to the operation.
* @param originator Value supplied to the operation.
* @returns The add unauthenticated result.
*/
addUnauthenticated(
connId: ConnectionId,
socket: WebSocketRef,
originator: Originator,
): Effect.Effect<void> {
return Ref.update(this.stateRef, (state) => ({
...state,
connections: HashMap.set(
state.connections,
connId,
new UnauthenticatedConnection({ connId, socket, originator }),
),
}));
}
/**
* Non-mutating read. Callers discriminate on the returned arm's `_tag`.
* @param connId Value supplied to the operation.
* @returns The current result.
*/
peek(connId: ConnectionId): Effect.Effect<Option.Option<Connection>> {
return Ref.get(this.stateRef).pipe(
Effect.map((state) => HashMap.get(state.connections, connId)),
);
}
/**
* Snapshot of every connection arm. Callers iterate + discriminate on `_tag`
* (e.g. The shutdown loop reads `arm.socket.shutdown`).
* @returns The current result.
*/
allConnections(): Effect.Effect<readonly Connection[]> {
return Ref.get(this.stateRef).pipe(
Effect.map((state) => Array.from(HashMap.values(state.connections))),
);
}
/**
* Current connection count.
* @returns The current result.
*/
currentSize(): Effect.Effect<number> {
return Ref.get(this.stateRef).pipe(
Effect.map((state) => HashMap.size(state.connections)),
);
}
/**
* Atomic per-connection authentication gate. Mints the agent arm from the
* unauthenticated entry and returns a `TransitionOutcome` whose success arm
* carries the minted connection, so callers narrow without a cast.
* @param connId Value supplied to the operation.
* @param auth Value supplied to the operation.
* @returns The current result.
*/
authenticate(
connId: ConnectionId,
auth: AgentContext,
): Effect.Effect<TransitionOutcome> {
return Ref.modify(this.stateRef, (state) => {
const current = HashMap.get(state.connections, connId);
if (Option.isNone(current)) {
return [{ kind: "not-connected" } as const, state];
}
return Match.value(current.value).pipe(
Match.tag(
"AgentConnection",
(existing): [TransitionOutcome, typeof state] => [
{ kind: "already-connected", existing },
state,
],
),
Match.tag(
"UnauthenticatedConnection",
(unauth): [TransitionOutcome, typeof state] => {
const authed = new AgentConnection({
connId: unauth.connId,
socket: unauth.socket,
originator: unauth.originator,
auth,
});
return [
{ kind: "ok-agent", authed },
{
...state,
connections: HashMap.set(state.connections, connId, authed),
},
];
},
),
Match.exhaustive,
);
});
}