agent-comms 5.0.6 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +48 -29
- package/dist/bridges/cc-peer/run.d.ts.map +1 -1
- package/dist/bridges/cc-peer/run.js +11 -13
- package/dist/bridges/cc-peer/run.js.map +1 -1
- package/dist/bridges/claude-code/channel.d.ts.map +1 -1
- package/dist/bridges/claude-code/channel.js +2 -6
- package/dist/bridges/claude-code/channel.js.map +1 -1
- package/dist/bridges/codex/tool.d.ts.map +1 -1
- package/dist/bridges/codex/tool.js +12 -2
- package/dist/bridges/codex/tool.js.map +1 -1
- package/dist/bridges/mcp/server.d.ts.map +1 -1
- package/dist/bridges/mcp/server.js +12 -2
- package/dist/bridges/mcp/server.js.map +1 -1
- package/dist/bridges/opencode/plugin.d.ts.map +1 -1
- package/dist/bridges/opencode/plugin.js +13 -2
- package/dist/bridges/opencode/plugin.js.map +1 -1
- package/dist/bridges/user/tui.d.ts.map +1 -1
- package/dist/bridges/user/tui.js +2 -1
- package/dist/bridges/user/tui.js.map +1 -1
- package/dist/bridges/user/web/contract.d.ts +201 -13
- package/dist/bridges/user/web/contract.d.ts.map +1 -1
- package/dist/bridges/user/web/dist/bundle.js +1 -1
- package/dist/bridges/user/web/dist/mesh-worker.js +1 -1
- package/dist/bridges/user/web/dist/sw.js +1 -1
- package/dist/bridges/user/web/frontend/messages.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/messages.js +2 -1
- package/dist/bridges/user/web/frontend/messages.js.map +1 -1
- package/dist/bridges/user/web/frontend/tab-contract.d.ts +60 -4
- package/dist/bridges/user/web/frontend/tab-contract.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/types.d.ts +2 -2
- package/dist/bridges/user/web/frontend/types.d.ts.map +1 -1
- package/dist/bridges/user/web/router.d.ts +60 -4
- package/dist/bridges/user/web/router.d.ts.map +1 -1
- package/dist/core/bind-retry.d.ts +4 -0
- package/dist/core/bind-retry.d.ts.map +1 -1
- package/dist/core/bind-retry.js +11 -2
- package/dist/core/bind-retry.js.map +1 -1
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +2 -1
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/comms-store.d.ts +18 -3
- package/dist/core/comms-store.d.ts.map +1 -1
- package/dist/core/delivery-engine.d.ts +3 -2
- package/dist/core/delivery-engine.d.ts.map +1 -1
- package/dist/core/delivery-engine.js +6 -4
- package/dist/core/delivery-engine.js.map +1 -1
- package/dist/core/gossip-directory.d.ts +3 -0
- package/dist/core/gossip-directory.d.ts.map +1 -1
- package/dist/core/gossip-directory.js +7 -0
- package/dist/core/gossip-directory.js.map +1 -1
- package/dist/core/hub-session.d.ts +8 -1
- package/dist/core/hub-session.d.ts.map +1 -1
- package/dist/core/hub-session.js +23 -22
- package/dist/core/hub-session.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/listener-registry.d.ts +8 -0
- package/dist/core/listener-registry.d.ts.map +1 -1
- package/dist/core/listener-registry.js +15 -0
- package/dist/core/listener-registry.js.map +1 -1
- package/dist/core/mesh-store-shared.d.ts +3 -0
- package/dist/core/mesh-store-shared.d.ts.map +1 -1
- package/dist/core/mesh-store-shared.js +8 -0
- package/dist/core/mesh-store-shared.js.map +1 -1
- package/dist/core/mesh-store.d.ts +7 -5
- package/dist/core/mesh-store.d.ts.map +1 -1
- package/dist/core/mesh-store.js +25 -50
- package/dist/core/mesh-store.js.map +1 -1
- package/dist/core/peer-lifecycle.d.ts +15 -1
- package/dist/core/peer-lifecycle.d.ts.map +1 -1
- package/dist/core/peer-lifecycle.js +84 -2
- package/dist/core/peer-lifecycle.js.map +1 -1
- package/dist/core/room-messaging.d.ts +8 -4
- package/dist/core/room-messaging.d.ts.map +1 -1
- package/dist/core/room-messaging.js +41 -18
- package/dist/core/room-messaging.js.map +1 -1
- package/dist/core/room-protocol.d.ts +17 -6
- package/dist/core/room-protocol.d.ts.map +1 -1
- package/dist/core/room-protocol.js +100 -18
- package/dist/core/room-protocol.js.map +1 -1
- package/dist/core/send-outcome.d.ts +28 -0
- package/dist/core/send-outcome.d.ts.map +1 -0
- package/dist/core/send-outcome.js +49 -0
- package/dist/core/send-outcome.js.map +1 -0
- package/dist/core/shutdown-signals.d.ts +22 -0
- package/dist/core/shutdown-signals.d.ts.map +1 -0
- package/dist/core/shutdown-signals.js +40 -0
- package/dist/core/shutdown-signals.js.map +1 -0
- package/dist/core/store.d.ts +4 -4
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +11 -2
- package/dist/core/store.js.map +1 -1
- package/dist/core/test/bridge.unit.test.js +1 -1
- package/dist/core/test/bridge.unit.test.js.map +1 -1
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +19 -5
- package/dist/core/tool.js.map +1 -1
- package/dist/core/transport-events.d.ts +19 -0
- package/dist/core/transport-events.d.ts.map +1 -0
- package/dist/core/transport-events.js +46 -0
- package/dist/core/transport-events.js.map +1 -0
- package/dist/core/transport.d.ts +6 -0
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/types.d.ts +90 -7
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +23 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/wire-mesh-transport.d.ts +5 -0
- package/dist/core/wire-mesh-transport.d.ts.map +1 -1
- package/dist/core/wire-mesh-transport.js +49 -58
- package/dist/core/wire-mesh-transport.js.map +1 -1
- package/dist/test/agent-comms-version-gossip.integration.test.js +1 -0
- package/dist/test/agent-comms-version-gossip.integration.test.js.map +1 -1
- package/dist/test/agent-self-gossip.integration.test.js +1 -0
- package/dist/test/agent-self-gossip.integration.test.js.map +1 -1
- package/dist/test/become-coordinator-actual-port.integration.test.js +1 -0
- package/dist/test/become-coordinator-actual-port.integration.test.js.map +1 -1
- package/dist/test/cc-peer-fronted-approval.integration.test.js +1 -1
- package/dist/test/cc-peer-fronted-approval.integration.test.js.map +1 -1
- package/dist/test/coordinator-failover.integration.test.d.ts +5 -0
- package/dist/test/coordinator-failover.integration.test.d.ts.map +1 -0
- package/dist/test/coordinator-failover.integration.test.js +127 -0
- package/dist/test/coordinator-failover.integration.test.js.map +1 -0
- package/dist/test/coordinator-failover.unit.test.d.ts +5 -0
- package/dist/test/coordinator-failover.unit.test.d.ts.map +1 -0
- package/dist/test/coordinator-failover.unit.test.js +231 -0
- package/dist/test/coordinator-failover.unit.test.js.map +1 -0
- package/dist/test/data-sync-frame-handling.integration.test.js +1 -0
- package/dist/test/data-sync-frame-handling.integration.test.js.map +1 -1
- package/dist/test/delivery-engine-delivery.unit.test.js +3 -2
- package/dist/test/delivery-engine-delivery.unit.test.js.map +1 -1
- package/dist/test/delivery-engine-directed-notify.unit.test.js +3 -2
- package/dist/test/delivery-engine-directed-notify.unit.test.js.map +1 -1
- package/dist/test/delivery-engine.unit.test.js +1 -0
- package/dist/test/delivery-engine.unit.test.js.map +1 -1
- package/dist/test/delivery-receipt.helper.js +3 -3
- package/dist/test/delivery-receipt.helper.js.map +1 -1
- package/dist/test/dm-delivery-status.integration.test.d.ts +5 -0
- package/dist/test/dm-delivery-status.integration.test.d.ts.map +1 -0
- package/dist/test/dm-delivery-status.integration.test.js +77 -0
- package/dist/test/dm-delivery-status.integration.test.js.map +1 -0
- package/dist/test/dm-first-contact.integration.test.js +3 -3
- package/dist/test/dm-first-contact.integration.test.js.map +1 -1
- package/dist/test/gateway-forwarding.integration.test.js +2 -2
- package/dist/test/gateway-forwarding.integration.test.js.map +1 -1
- package/dist/test/gateway-trust.integration.test.js +1 -1
- package/dist/test/gateway-trust.integration.test.js.map +1 -1
- package/dist/test/gossip-directory-aggregation.integration.test.js +1 -0
- package/dist/test/gossip-directory-aggregation.integration.test.js.map +1 -1
- package/dist/test/gossip-listener-addresses.integration.test.js +1 -0
- package/dist/test/gossip-listener-addresses.integration.test.js.map +1 -1
- package/dist/test/hosted-rooms-gossip.integration.test.js +1 -0
- package/dist/test/hosted-rooms-gossip.integration.test.js.map +1 -1
- package/dist/test/hub-mode-session.integration.test.js +1 -0
- package/dist/test/hub-mode-session.integration.test.js.map +1 -1
- package/dist/test/hub-session-close.integration.test.d.ts +5 -0
- package/dist/test/hub-session-close.integration.test.d.ts.map +1 -0
- package/dist/test/hub-session-close.integration.test.js +38 -0
- package/dist/test/hub-session-close.integration.test.js.map +1 -0
- package/dist/test/hub-session-dispatch.unit.test.js +1 -0
- package/dist/test/hub-session-dispatch.unit.test.js.map +1 -1
- package/dist/test/hub-todevice-routing.integration.test.js +2 -2
- package/dist/test/hub-todevice-routing.integration.test.js.map +1 -1
- package/dist/test/listener-policy.integration.test.js +1 -0
- package/dist/test/listener-policy.integration.test.js.map +1 -1
- package/dist/test/mesh-graph-trace.integration.test.js +1 -0
- package/dist/test/mesh-graph-trace.integration.test.js.map +1 -1
- package/dist/test/mesh-store-orchestration.unit.test.js +1 -0
- package/dist/test/mesh-store-orchestration.unit.test.js.map +1 -1
- package/dist/test/peer-lifecycle.unit.test.js +9 -1
- package/dist/test/peer-lifecycle.unit.test.js.map +1 -1
- package/dist/test/presence-readvertise.integration.test.js +1 -0
- package/dist/test/presence-readvertise.integration.test.js.map +1 -1
- package/dist/test/query-version.integration.test.js +4 -2
- package/dist/test/query-version.integration.test.js.map +1 -1
- package/dist/test/room-join-admission.integration.test.js +1 -0
- package/dist/test/room-join-admission.integration.test.js.map +1 -1
- package/dist/test/room-join-approval-timeout.integration.test.js +1 -1
- package/dist/test/room-join-approval-timeout.integration.test.js.map +1 -1
- package/dist/test/room-messaging-durable-send.unit.test.js +3 -1
- package/dist/test/room-messaging-durable-send.unit.test.js.map +1 -1
- package/dist/test/room-messaging.unit.test.js +106 -15
- package/dist/test/room-messaging.unit.test.js.map +1 -1
- package/dist/test/room-protocol-admission.unit.test.js +9 -161
- package/dist/test/room-protocol-admission.unit.test.js.map +1 -1
- package/dist/test/room-protocol-retry-queue.unit.test.d.ts +2 -0
- package/dist/test/room-protocol-retry-queue.unit.test.d.ts.map +1 -0
- package/dist/test/room-protocol-retry-queue.unit.test.js +275 -0
- package/dist/test/room-protocol-retry-queue.unit.test.js.map +1 -0
- package/dist/test/room-protocol.helper.d.ts +60 -0
- package/dist/test/room-protocol.helper.d.ts.map +1 -0
- package/dist/test/room-protocol.helper.js +168 -0
- package/dist/test/room-protocol.helper.js.map +1 -0
- package/dist/test/room-protocol.unit.test.js +4 -159
- package/dist/test/room-protocol.unit.test.js.map +1 -1
- package/dist/test/room-read-directed.integration.test.js +3 -2
- package/dist/test/room-read-directed.integration.test.js.map +1 -1
- package/dist/test/room-router.unit.test.js +1 -0
- package/dist/test/room-router.unit.test.js.map +1 -1
- package/dist/test/room-send-retry-queue.integration.test.js +1 -0
- package/dist/test/room-send-retry-queue.integration.test.js.map +1 -1
- package/dist/test/send-outcome.unit.test.d.ts +2 -0
- package/dist/test/send-outcome.unit.test.d.ts.map +1 -0
- package/dist/test/send-outcome.unit.test.js +52 -0
- package/dist/test/send-outcome.unit.test.js.map +1 -0
- package/dist/test/shutdown-signals.unit.test.d.ts +5 -0
- package/dist/test/shutdown-signals.unit.test.d.ts.map +1 -0
- package/dist/test/shutdown-signals.unit.test.js +110 -0
- package/dist/test/shutdown-signals.unit.test.js.map +1 -0
- package/dist/test/test-transport.d.ts +2 -2
- package/dist/test/test-transport.d.ts.map +1 -1
- package/dist/test/test-transport.js +2 -2
- package/dist/test/test-transport.js.map +1 -1
- package/dist/test/wire-mesh-transport-gateway-trust.integration.test.js +4 -2
- package/dist/test/wire-mesh-transport-gateway-trust.integration.test.js.map +1 -1
- package/dist/test/wire-mesh-transport-hub.integration.test.js +1 -0
- package/dist/test/wire-mesh-transport-hub.integration.test.js.map +1 -1
- package/dist/test/wire-mesh-transport-shutdown-unref.integration.test.js +1 -0
- package/dist/test/wire-mesh-transport-shutdown-unref.integration.test.js.map +1 -1
- package/dist/test/wire-mesh-transport.integration.test.js +1 -0
- package/dist/test/wire-mesh-transport.integration.test.js.map +1 -1
- package/dist/test/wss-remote-connect.integration.test.js +1 -0
- package/dist/test/wss-remote-connect.integration.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
* PeerLifecycle — the TransportEvents callbacks around a peer's own comings and goings on the mesh: peer-list/peer-joined bookkeeping, the coordinator's own new-peer introduction handshake, post-connection state-sync, inbound data-message routing (state_sync/state_update), taking over as coordinator, and peer disconnection. Split out of mesh-store.ts to reduce it under the repo's max-lines cap.
|
|
3
3
|
*/
|
|
4
4
|
import { normaliseWireState } from "./wire-protocol.js";
|
|
5
|
+
import { isAddrInUse, isConnectionRefused } from "./bind-retry.js";
|
|
5
6
|
import { COORDINATOR_HOST } from "./mesh-store-shared.js";
|
|
7
|
+
/** The message an unknown thrown value is reported as, so a takeover or rejoin failure reads the same whether the transport rejected with an Error or something else. */
|
|
8
|
+
function describe(error) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
/** How many times a peer that has lost its coordinator alternates between binding the vacated port and joining whoever bound it first. More than one round is genuinely needed: a graceful handover names its successor before the outgoing coordinator's own listener closes, so a bind attempted in that window fails on a port that is about to be free, and the join it falls back to finds nothing listening a moment later. */
|
|
12
|
+
const COORDINATOR_TAKEOVER_ROUNDS = 5;
|
|
6
13
|
export class PeerLifecycle {
|
|
7
14
|
deps;
|
|
15
|
+
/** Guards against a second contest running while the first is still binding or rejoining -- a coordinator whose process dies can close more than one session to this side, and each close arrives as its own disconnect. */
|
|
16
|
+
contestingCoordinatorRole = false;
|
|
8
17
|
constructor(deps) {
|
|
9
18
|
this.deps = deps;
|
|
10
19
|
}
|
|
@@ -70,8 +79,12 @@ export class PeerLifecycle {
|
|
|
70
79
|
await this.deps
|
|
71
80
|
.requireTransport()
|
|
72
81
|
.becomeCoordinator(COORDINATOR_HOST, this.deps.coordinatorPort);
|
|
73
|
-
this.deps.peerInfo.clear();
|
|
74
82
|
const peerId = this.deps.getPeerId();
|
|
83
|
+
// peerList never names the successor itself (neither the graceful handover nor the crash race includes it), so this side's own entry is carried across the reset rather than dropped: handleIntroduction answers every later joiner with exactly these entries, and a joiner that is never told the coordinator's own data port never dials it.
|
|
84
|
+
const self = this.deps.peerInfo.get(peerId);
|
|
85
|
+
this.deps.peerInfo.clear();
|
|
86
|
+
if (self !== undefined)
|
|
87
|
+
this.deps.peerInfo.set(peerId, self);
|
|
75
88
|
for (const peer of peerList) {
|
|
76
89
|
this.deps.peerInfo.set(peer.id, peer);
|
|
77
90
|
void this.deps.requireTransport().connectToPeer(peer, peerId);
|
|
@@ -81,7 +94,76 @@ export class PeerLifecycle {
|
|
|
81
94
|
await this.deps.onCoordinatorRoleChanged?.();
|
|
82
95
|
}
|
|
83
96
|
handlePeerDisconnected(handle) {
|
|
84
|
-
this.
|
|
97
|
+
void this.handlePeerDeparture(handle.id);
|
|
98
|
+
}
|
|
99
|
+
/** The awaitable form of handlePeerDisconnected, which TransportEvents forces to be synchronous. A departing peer leaves the peer list; when it was this side's own coordinator, its vacated role is contested before anything else, so whoever wins is already coordinator by the time the departed peer's agent record is retired. */
|
|
100
|
+
async handlePeerDeparture(peerId) {
|
|
101
|
+
this.deps.peerInfo.delete(peerId);
|
|
102
|
+
if (peerId === this.deps.getCoordinatorPeerId()) {
|
|
103
|
+
await this.contestCoordinatorRole(peerId);
|
|
104
|
+
}
|
|
105
|
+
await this.retireDepartedAgent(peerId);
|
|
106
|
+
}
|
|
107
|
+
/** Races every other surviving peer to rebind the coordinator port, which is what makes a coordinator CRASH recoverable at all: unlike the graceful handover, a killed coordinator names no successor, so each survivor contests independently and the operating system's own exclusive bind decides the single winner. The winner runs the ordinary takeover path (handleBecomeCoordinator) over the peers it still holds connections to; a loser -- the one case where the bind fails specifically because the port is already taken -- re-introduces itself to the winner so it is a full member of the new mesh again, with its own data port known to whoever joins next. Direct peer-to-peer data connections are untouched throughout, so traffic between survivors never depends on the outcome of this race. */
|
|
108
|
+
async contestCoordinatorRole(lostCoordinatorId) {
|
|
109
|
+
const transport = this.deps.requireTransport();
|
|
110
|
+
if (transport.isCoordinator || this.contestingCoordinatorRole)
|
|
111
|
+
return;
|
|
112
|
+
this.contestingCoordinatorRole = true;
|
|
113
|
+
try {
|
|
114
|
+
const selfId = this.deps.getPeerId();
|
|
115
|
+
const survivors = [...this.deps.peerInfo.values()].filter((peer) => peer.id !== selfId && peer.id !== lostCoordinatorId);
|
|
116
|
+
for (let round = 0; round < COORDINATOR_TAKEOVER_ROUNDS; round++) {
|
|
117
|
+
try {
|
|
118
|
+
await this.handleBecomeCoordinator(survivors);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!isAddrInUse(error)) {
|
|
123
|
+
this.deps.onError?.(new Error(`PeerLifecycle: could not take over the vacated coordinator role: ${describe(error)}`));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// A graceful handover racing this crash race can have made this side the coordinator while the bind above was in flight: the bind then fails against this side's own freshly bound listener, and dialling it would be this peer introducing itself to itself. Re-read through requireTransport() rather than the narrowed local, which TypeScript still believes is false from the guard at the top of this method.
|
|
128
|
+
if (this.deps.requireTransport().isCoordinator)
|
|
129
|
+
return;
|
|
130
|
+
if ((await this.rejoinUnderNewCoordinator(selfId)) !== "nothing-listening")
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.deps.onError?.(new Error("PeerLifecycle: the vacated coordinator port kept changing hands and this peer neither took it nor found anyone holding it"));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.contestingCoordinatorRole = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Re-introduces this side to whichever survivor won the bind race, over a fresh coordinator connection to the same well-known port. Without it this peer would keep its existing data connections but be unknown to the new coordinator, so it would never appear in the peer list handed to the next joiner. A refused dial is reported as "nothing-listening" rather than as an error: it means the winner released the port again (or never finished taking it), which the caller answers by re-entering the bind race. Any other failure is reported here, since it says the coordinator is there and this peer still cannot reach it. */
|
|
140
|
+
async rejoinUnderNewCoordinator(selfId) {
|
|
141
|
+
const transport = this.deps.requireTransport();
|
|
142
|
+
try {
|
|
143
|
+
await transport.connectToCoordinator(COORDINATOR_HOST, this.deps.coordinatorPort, selfId, transport.dataPort);
|
|
144
|
+
return "joined";
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (isConnectionRefused(error))
|
|
148
|
+
return "nothing-listening";
|
|
149
|
+
this.deps.onError?.(new Error(`PeerLifecycle: lost the coordinator bind race and could not rejoin under the new coordinator: ${describe(error)}`));
|
|
150
|
+
return "failed";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Marks a departed peer's own agent offline and announces it, but only from the coordinator -- the same single-authority rule StaleAgentChecker's PID probe already follows, so a departure produces one announcement rather than one per surviving peer. An agent's id is its peer's id (AgentRegistry.registerAgent), so the departed peer names its own record directly; the PID probe remains the backstop for an agent whose process dies without its session closing. */
|
|
154
|
+
async retireDepartedAgent(peerId) {
|
|
155
|
+
if (!this.deps.requireTransport().isCoordinator)
|
|
156
|
+
return;
|
|
157
|
+
const agent = this.deps.agents.get(peerId);
|
|
158
|
+
if (agent === undefined || agent.status === "offline")
|
|
159
|
+
return;
|
|
160
|
+
agent.status = "offline";
|
|
161
|
+
this.deps.agents.set(peerId, agent);
|
|
162
|
+
await this.deps.deliveryEngine.notifyRoomsOfStatus(peerId, "offline");
|
|
163
|
+
await this.deps.deliveryEngine.broadcastPatch({
|
|
164
|
+
type: "agent_offline",
|
|
165
|
+
agentId: peerId,
|
|
166
|
+
});
|
|
85
167
|
}
|
|
86
168
|
/** Graceful coordinator handover (agent-comms#170): called by MeshStore.shutdown() while the transport is still up. A no-op unless this side currently holds the coordinator role and at least one other peer remains connected -- neither condition is this class's own business to log or report on, since a solo coordinator shutting down or a non-coordinator peer shutting down are both entirely ordinary. When both hold, picks the longest-running remaining peer (the one with the earliest recorded PeerInfo.startedAt, matching the README's documented policy) as the successor and sends it become_coordinator carrying every other remaining peer -- exactly the peerList shape handleBecomeCoordinator already expects (it dials each entry itself; the successor doesn't need to be told about itself). The crash-race path (each surviving peer independently racing to rebind the coordinator port) is a separate mechanism and untouched by this method. */
|
|
87
169
|
async sendCoordinatorHandover() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer-lifecycle.js","sourceRoot":"","sources":["../../src/core/peer-lifecycle.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAMxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"peer-lifecycle.js","sourceRoot":"","sources":["../../src/core/peer-lifecycle.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { MeshStoreIdentity } from "./mesh-store-shared.js";
|
|
5
5
|
import type { RoomProtocol } from "./room-protocol.js";
|
|
6
6
|
import type { AgentIdentity, DmMessage, Room, RoomMessage, StreamingBehavior } from "./types.js";
|
|
7
|
-
import type { SendRoomMessageOptions } from "./comms-store.js";
|
|
7
|
+
import type { SentDm, SentRoomMessage, SendRoomMessageOptions } from "./comms-store.js";
|
|
8
8
|
/** The state and collaborators RoomMessaging needs from MeshStore. rooms/messages/dms are direct references into MeshStore's own fields; roomProtocol is the already-constructed instance (construction order: ... -\> roomProtocol -\> roomMessaging -\> ...), narrowed to what sending a message or DM ever needs; resolveAgent is AgentRegistry's own getAgent (deferred the same lazy-closure way DeliveryEngine's sendRoomRequestToMember closure is, since AgentRegistry doesn't exist yet at RoomMessaging's own construction point) rather than a bare `agents.get` lookup, so sendDm's own existence check also resolves a gossip-discovered agent (a remote, hub-learned one included, agent-comms#155) that will never appear in the agents map directly. */
|
|
9
9
|
export interface RoomMessagingDeps {
|
|
10
10
|
rooms: Map<string, Room>;
|
|
@@ -29,13 +29,17 @@ export declare class RoomMessaging {
|
|
|
29
29
|
* Sends a room message via a real, wire-authenticated room.send fan-out (P3.5): one directed request per member, each carrying this sender's own persisted room:member token, rather than the legacy broadcastPatch's full-state replication. A member unreachable right now is queued for retry (see sendRoomRequestToMember/flushPendingRoomRequests) instead of blocking or failing the whole send -- delivery to any one recipient is independent of every other.
|
|
30
30
|
*
|
|
31
31
|
* durable, when true, additionally records this same message as a room-notice in the sender's own oplog via recordRoomSendNotice (P5, agent-comms#50) -- deliberately opt-in per call, not automatic: matching this codebase's own design principle that delivery and durable catch-up are the same artifact only when a caller actually opts a message into it. A caller that wants an offline member to be able to catch up on this specific message later passes true; every other send stays exactly as before.
|
|
32
|
+
*
|
|
33
|
+
* Returns the message alongside one delivery state per remote member, so the caller can tell who actually received it from who it is merely queued for. A member that refuses the message outright raises: a refusal is the member's settled answer, not a state to report as pending, and a fan-out whose members cannot all be reported honestly is not a successful send.
|
|
32
34
|
*/
|
|
33
|
-
sendRoomMessage(roomIdOrName: string, from: string, content: string, options?: RoomMessagingSendOptions): Promise<
|
|
35
|
+
sendRoomMessage(roomIdOrName: string, from: string, content: string, options?: RoomMessagingSendOptions): Promise<SentRoomMessage>;
|
|
34
36
|
readRoomMessages(roomIdOrName: string, since?: string): Promise<RoomMessage[]>;
|
|
35
37
|
/**
|
|
36
|
-
* Sends a DM via the same wire-authenticated room.send fan-out sendRoomMessage uses (P3.5): a DM is just a dm-shaped room path with exactly one other member, so it rides the identical mechanism rather than a separate one. Self-DM is the one exception -- a purely local scratchpad note that never leaves the process, so it needs no token and no wire round trip at all.
|
|
38
|
+
* Sends a DM via the same wire-authenticated room.send fan-out sendRoomMessage uses (P3.5): a DM is just a dm-shaped room path with exactly one other member, so it rides the identical mechanism rather than a separate one. Self-DM is the one exception -- a purely local scratchpad note that never leaves the process, so it needs no token and no wire round trip at all, and is delivered the moment it is recorded.
|
|
39
|
+
*
|
|
40
|
+
* Returns the message alongside what actually became of it, so a caller can never mistake a message merely held for retry for one the recipient received. A recipient that refuses the message raises SEND_REFUSED naming its own reason: a DM has exactly one recipient, so its refusal is the whole send failing, and no retry will change it.
|
|
37
41
|
*/
|
|
38
|
-
sendDm(from: string, to: string, content: string, streamingBehavior?: StreamingBehavior): Promise<
|
|
42
|
+
sendDm(from: string, to: string, content: string, streamingBehavior?: StreamingBehavior): Promise<SentDm>;
|
|
39
43
|
/** The room:member token this device holds for its DM path with `to`, requesting DM access from `to` first when none is persisted yet. Throws NOT_MEMBER if the request resolves without a token having been persisted, since no send can be authenticated without one. */
|
|
40
44
|
private dmRoomMemberToken;
|
|
41
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"room-messaging.d.ts","sourceRoot":"","sources":["../../src/core/room-messaging.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"room-messaging.d.ts","sourceRoot":"","sources":["../../src/core/room-messaging.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EAET,IAAI,EACJ,WAAW,EACX,iBAAiB,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAEV,MAAM,EACN,eAAe,EACf,sBAAsB,EACvB,MAAM,kBAAkB,CAAC;AAW1B,wuBAAwuB;AACxuB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACrC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9B,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;IAC5D,6PAA6P;IAC7P,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,+SAA+S;IAC/S,aAAa,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;CAClE;AAED,sMAAsM;AACtM,MAAM,WAAW,wBAAyB,SAAQ,sBAAsB;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,iBAAiB;IAEpD;;;;;;OAMG;IACG,eAAe,CACnB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,eAAe,CAAC;IA+ErB,gBAAgB,CACpB,YAAY,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC;IAQzB;;;;OAIG;IACG,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,OAAO,CAAC,MAAM,CAAC;IA6DlB,2QAA2Q;YAC7P,iBAAiB;CAgBhC"}
|
|
@@ -7,6 +7,13 @@ import { loadRoomTokens } from "./identity-store.js";
|
|
|
7
7
|
import { randomId } from "./random-id.js";
|
|
8
8
|
import { recordRoomSendNotice } from "./room-notice-log.js";
|
|
9
9
|
import { CommsError } from "./store.js";
|
|
10
|
+
import { describeRefusal } from "./send-outcome.js";
|
|
11
|
+
/** Turns one member's send outcome into the delivery state its sender is told about. A refusal is not represented here at all: a refused send raises rather than resolving, so this only ever maps the two states a resolved send can be in. */
|
|
12
|
+
function deliveryFromOutcome(outcome) {
|
|
13
|
+
return outcome.kind === "delivered"
|
|
14
|
+
? { status: "delivered" }
|
|
15
|
+
: { status: "queued", reason: outcome.reason };
|
|
16
|
+
}
|
|
10
17
|
export class RoomMessaging {
|
|
11
18
|
deps;
|
|
12
19
|
constructor(deps) {
|
|
@@ -16,6 +23,8 @@ export class RoomMessaging {
|
|
|
16
23
|
* Sends a room message via a real, wire-authenticated room.send fan-out (P3.5): one directed request per member, each carrying this sender's own persisted room:member token, rather than the legacy broadcastPatch's full-state replication. A member unreachable right now is queued for retry (see sendRoomRequestToMember/flushPendingRoomRequests) instead of blocking or failing the whole send -- delivery to any one recipient is independent of every other.
|
|
17
24
|
*
|
|
18
25
|
* durable, when true, additionally records this same message as a room-notice in the sender's own oplog via recordRoomSendNotice (P5, agent-comms#50) -- deliberately opt-in per call, not automatic: matching this codebase's own design principle that delivery and durable catch-up are the same artifact only when a caller actually opts a message into it. A caller that wants an offline member to be able to catch up on this specific message later passes true; every other send stays exactly as before.
|
|
26
|
+
*
|
|
27
|
+
* Returns the message alongside one delivery state per remote member, so the caller can tell who actually received it from who it is merely queued for. A member that refuses the message outright raises: a refusal is the member's settled answer, not a state to report as pending, and a fan-out whose members cannot all be reported honestly is not a successful send.
|
|
19
28
|
*/
|
|
20
29
|
async sendRoomMessage(roomIdOrName, from, content, options) {
|
|
21
30
|
const { replyTo, streamingBehavior, durable } = options ?? {};
|
|
@@ -65,12 +74,20 @@ export class RoomMessaging {
|
|
|
65
74
|
"streaming-behavior": streamingBehavior,
|
|
66
75
|
}),
|
|
67
76
|
};
|
|
77
|
+
const deliveries = [];
|
|
68
78
|
for (const memberId of room.members) {
|
|
69
|
-
if (memberId
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
if (memberId === from)
|
|
80
|
+
continue;
|
|
81
|
+
const outcome = await this.deps.roomProtocol.sendRoomRequestToMember(memberId, roomId, token, params);
|
|
82
|
+
// A refusal is reported as this member's own delivery state rather than raised, unlike sendDm's single recipient: delivery to any one member is independent of every other, so one member's refusal must neither stop the message reaching the rest nor discard what became of them.
|
|
83
|
+
deliveries.push({
|
|
84
|
+
member: memberId,
|
|
85
|
+
delivery: outcome.kind === "refused"
|
|
86
|
+
? { status: "refused", code: describeRefusal(outcome) }
|
|
87
|
+
: deliveryFromOutcome(outcome),
|
|
88
|
+
});
|
|
72
89
|
}
|
|
73
|
-
return message;
|
|
90
|
+
return { message, deliveries };
|
|
74
91
|
}
|
|
75
92
|
async readRoomMessages(roomIdOrName, since) {
|
|
76
93
|
await Promise.resolve();
|
|
@@ -81,7 +98,9 @@ export class RoomMessaging {
|
|
|
81
98
|
return arr.filter((m) => m.timestamp > since);
|
|
82
99
|
}
|
|
83
100
|
/**
|
|
84
|
-
* Sends a DM via the same wire-authenticated room.send fan-out sendRoomMessage uses (P3.5): a DM is just a dm-shaped room path with exactly one other member, so it rides the identical mechanism rather than a separate one. Self-DM is the one exception -- a purely local scratchpad note that never leaves the process, so it needs no token and no wire round trip at all.
|
|
101
|
+
* Sends a DM via the same wire-authenticated room.send fan-out sendRoomMessage uses (P3.5): a DM is just a dm-shaped room path with exactly one other member, so it rides the identical mechanism rather than a separate one. Self-DM is the one exception -- a purely local scratchpad note that never leaves the process, so it needs no token and no wire round trip at all, and is delivered the moment it is recorded.
|
|
102
|
+
*
|
|
103
|
+
* Returns the message alongside what actually became of it, so a caller can never mistake a message merely held for retry for one the recipient received. A recipient that refuses the message raises SEND_REFUSED naming its own reason: a DM has exactly one recipient, so its refusal is the whole send failing, and no retry will change it.
|
|
85
104
|
*/
|
|
86
105
|
async sendDm(from, to, content, streamingBehavior) {
|
|
87
106
|
if (to !== from) {
|
|
@@ -109,20 +128,24 @@ export class RoomMessaging {
|
|
|
109
128
|
const arr = this.deps.dms.get(key) ?? [];
|
|
110
129
|
arr.push(message);
|
|
111
130
|
this.deps.dms.set(key, arr);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
// A self-DM never leaves the process, so it is already exactly as delivered as it will ever be by the time it is recorded above.
|
|
132
|
+
if (token === undefined)
|
|
133
|
+
return { message, delivery: { status: "delivered" } };
|
|
134
|
+
const { clock } = this.deps.requireIdentity();
|
|
135
|
+
const params = {
|
|
136
|
+
verb: "room.send",
|
|
137
|
+
"message-id": messageId,
|
|
138
|
+
"sent-at": clock.now(),
|
|
139
|
+
text: content,
|
|
140
|
+
...(streamingBehavior !== undefined && {
|
|
141
|
+
"streaming-behavior": streamingBehavior,
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
const outcome = await this.deps.roomProtocol.sendRoomRequestToMember(to, key, token, params);
|
|
145
|
+
if (outcome.kind === "refused") {
|
|
146
|
+
throw new CommsError(`DM to ${to} refused (${describeRefusal(outcome)})`, "SEND_REFUSED");
|
|
124
147
|
}
|
|
125
|
-
return message;
|
|
148
|
+
return { message, delivery: deliveryFromOutcome(outcome) };
|
|
126
149
|
}
|
|
127
150
|
/** The room:member token this device holds for its DM path with `to`, requesting DM access from `to` first when none is persisted yet. Throws NOT_MEMBER if the request resolves without a token having been persisted, since no send can be authenticated without one. */
|
|
128
151
|
async dmRoomMemberToken(from, to) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"room-messaging.js","sourceRoot":"","sources":["../../src/core/room-messaging.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"room-messaging.js","sourceRoot":"","sources":["../../src/core/room-messaging.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* RoomProtocol — the receiving/responding half of the room wire protocol: the room-verb handlers a WireMeshTransport dispatches into (room.send, room.read, room.members, room.join, room.invite, room.leave), the directed room-domain request fan-out primitives (sendRoomMessageDirected, sendRoomRequestToMember, its retry queue and flush), and the human accept/reject decision points for a pending room.join. Split out of mesh-store.ts to reduce it under the repo's max-lines cap. Owns pendingRoomRequests and pendingRoomJoins exclusively -- nothing outside this class ever reads or writes them.
|
|
3
3
|
*/
|
|
4
4
|
import type { CapabilityToken } from "wire-mesh-core/generated/protocol";
|
|
5
|
+
import { type RoomRequestOutcome } from "./send-outcome.js";
|
|
5
6
|
import type { MeshStoreIdentity } from "./mesh-store-shared.js";
|
|
6
7
|
import type { DeliveryEngine } from "./delivery-engine.js";
|
|
7
8
|
import type { RoomVerbHandler } from "./room-router.js";
|
|
@@ -25,7 +26,7 @@ export interface RoomProtocolDeps {
|
|
|
25
26
|
}
|
|
26
27
|
export declare class RoomProtocol {
|
|
27
28
|
private readonly deps;
|
|
28
|
-
/** Directed room-domain requests (room.send, room.read) that failed because their target member wasn't reachable at send time, held for retry when that member
|
|
29
|
+
/** Directed room-domain requests (room.send, room.read) that failed because their target member wasn't reachable at send time, held for retry when that member becomes reachable by any route -- the wire-authenticated fan-out's substitute for the legacy full-state-sync's own automatic eventual consistency, since a direct request to a disconnected peer fails immediately with no protocol-level retry of its own. Keyed by member device-id hex, bounded oldest-first per member and by each entry's own age; every entry that leaves the queue without being delivered is reported to its sender as a terminal delivery status. */
|
|
29
30
|
private readonly pendingRoomRequests;
|
|
30
31
|
/** Pending room.join requests awaiting owner approval, keyed by `${roomPath}::${requesterId}` -- the held-open manage-request's own resolve() function is stored here so acceptRoomJoin/rejectRoomJoin can settle it in place. */
|
|
31
32
|
private readonly pendingRoomJoins;
|
|
@@ -56,13 +57,23 @@ export declare class RoomProtocol {
|
|
|
56
57
|
* Sends one directed room.send to a single member's own session, attaching this store's own persisted room:member token for the given room path -- the primitive P3.5's own directed fan-out (deliverToRoom) will loop over per member once it replaces the legacy broadcastPatch path this store still uses for message delivery today. Throws if this store holds no token for the room: never a member, or a token that expired or was revoked with nothing fresh persisted in its place.
|
|
57
58
|
*/
|
|
58
59
|
sendRoomMessageDirected(roomPath: string, memberId: string, text: string): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
private
|
|
60
|
+
/** Tells this device's own agent what became of a message it sent, over the same delivery_status event read receipts already ride, so a queued, dropped, expired or refused message reaches the sender through the one path that already reports delivery. Does nothing for a request carrying no message of this sender's own (a room.read receipt, a room.notify), which has no message-id to report against. */
|
|
61
|
+
private reportDelivery;
|
|
62
|
+
/** Holds a request for retry, evicting the oldest entries beyond the per-member bound and telling each evicted entry's sender the message was dropped, so a message given up on is never left looking merely pending. */
|
|
63
|
+
private enqueuePendingRoomRequest;
|
|
61
64
|
/**
|
|
62
|
-
*
|
|
65
|
+
* Gives up on every queued request older than the age bound, across every member, telling each one's sender it expired. Deliberately a sweep run from sendRoomRequestToMember and flushPendingRoomRequests rather than a timer: the queue only ever grows on a send and only ever drains on a flush, so those are exactly the moments an expired entry starts to matter, and nothing has to keep a timer alive for a member that may never come back.
|
|
66
|
+
*/
|
|
67
|
+
private expirePendingRoomRequests;
|
|
68
|
+
/** One attempt at a directed room-domain request, with no queueing of its own: the raw wire outcome classified into delivered, undelivered (nobody answered, so a retry may still work) or refused (the member answered with a failure it will repeat). A rejection from the transport is the connection dropping mid-request, per wire-mesh-core's own rejectPendingManageRequests, which is the same "never answered" fact as a timeout. */
|
|
69
|
+
private attemptRoomRequest;
|
|
70
|
+
/**
|
|
71
|
+
* Sends one directed room-domain request (room.send, room.read, or any future room:member-gated verb) to a single member and reports honestly what became of it -- the fan-out's own per-recipient primitive, distinct from sendRoomMessageDirected's deliberate throw-on-failure contract for a caller sending to one specific, known recipient. An undelivered request is held for retry and says so; a refused one is not queued at all, since the member has already answered with a failure it will give again, and its sender needs to know now rather than waiting on a retry that cannot help.
|
|
72
|
+
*/
|
|
73
|
+
sendRoomRequestToMember(memberId: string, roomPath: string, token: CapabilityToken, params: Record<string, unknown>): Promise<RoomRequestOutcome>;
|
|
74
|
+
/**
|
|
75
|
+
* Retries every request queued for memberId since it was last reachable, and tells each one's sender what came of the retry: delivered, refused, or still undelivered and held for another attempt. An entry whose room this store no longer holds a token for can never be sent at all, so it is reported as dropped rather than retried forever. Called whenever memberId becomes reachable by any route -- a direct peer connection, or the hub admitting its device into this side's directory -- since a queued request cares only that some path to the member now exists, not which one.
|
|
63
76
|
*/
|
|
64
|
-
sendRoomRequestToMember(memberId: string, roomPath: string, token: CapabilityToken, params: Record<string, unknown>): Promise<void>;
|
|
65
|
-
/** Retries every room.send queued for memberId since it was last reachable, dropping (not re-queuing) any whose room this store no longer holds a token for. Called once a connection to memberId is (re)established -- handlePeerConnected fires for both a fresh introduction and a reconnection after downtime, exactly the two cases a queued send needs to be retried on. */
|
|
66
77
|
flushPendingRoomRequests(memberId: string): Promise<void>;
|
|
67
78
|
/**
|
|
68
79
|
* Owner-side admission for an incoming room.join request, against either a named room this store owns or a DM path this store is a participant of. Named-room admission always needs a human decision; DM admission needs one only for the party being contacted first -- the reply half of the two-round consent flow (section 6) auto-approves, since a reply on a path this node itself opened is not unsolicited contact, and so does a request presenting a valid dm:send capability this node's own user principal already issued the requester (agent-comms#162's own durable admission list, checked before ever falling through to a fresh human decision).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"room-protocol.d.ts","sourceRoot":"","sources":["../../src/core/room-protocol.ts"],"names":[],"mappings":"AAAA;;GAEG;AAcH,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"room-protocol.d.ts","sourceRoot":"","sources":["../../src/core/room-protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,15 +11,23 @@ import { verifyDmSendToken } from "./dm-token-verification.js";
|
|
|
11
11
|
import { loadRoomTokens, saveIssuedRoomGrant, saveRoomToken, } from "./identity-store.js";
|
|
12
12
|
import { randomId } from "./random-id.js";
|
|
13
13
|
import { CommsError } from "./store.js";
|
|
14
|
-
import {
|
|
14
|
+
import { MAX_PENDING_ROOM_REQUESTS_PER_MEMBER, PENDING_ROOM_REQUEST_TTL_MS, ROOM_TOKEN_LIFETIME_MS, } from "./mesh-store-shared.js";
|
|
15
|
+
import { classifyManageOutcome, } from "./send-outcome.js";
|
|
15
16
|
import { parseInviterAgentExtension, parseRoomStateExtension, roomStateExtension, } from "./room-wire-extensions.js";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
import { DeliveryEventSchema, StreamingBehavior } from "./types.js";
|
|
19
|
+
/** The hex message-id a directed room-domain request carries, when it is a room.send carrying one. Read from the already-built params rather than re-derived, so a queued entry can report a delivery status against the very message the sender was told about. */
|
|
20
|
+
function messageIdFromParams(params) {
|
|
21
|
+
if (params.verb !== "room.send")
|
|
22
|
+
return undefined;
|
|
23
|
+
const raw = params["message-id"];
|
|
24
|
+
return raw instanceof Uint8Array ? bytesToHex(raw) : undefined;
|
|
25
|
+
}
|
|
18
26
|
/** room.notify's own params shape: the already-validated DeliveryEvent to deliver, and nothing else -- room.notify carries no content of its own beyond the event, unlike room.send's own message/dm fields. Not a wire-mesh-generated schema (room.notify is agent-comms' own verb, riding manage-command-params' open socket the same way the legacy opaque frame carriage always did, never a real wire-mesh CDDL type other clients need to interoperate with). */
|
|
19
27
|
const RoomNotifyParamsSchema = z.object({ event: DeliveryEventSchema });
|
|
20
28
|
export class RoomProtocol {
|
|
21
29
|
deps;
|
|
22
|
-
/** Directed room-domain requests (room.send, room.read) that failed because their target member wasn't reachable at send time, held for retry when that member
|
|
30
|
+
/** Directed room-domain requests (room.send, room.read) that failed because their target member wasn't reachable at send time, held for retry when that member becomes reachable by any route -- the wire-authenticated fan-out's substitute for the legacy full-state-sync's own automatic eventual consistency, since a direct request to a disconnected peer fails immediately with no protocol-level retry of its own. Keyed by member device-id hex, bounded oldest-first per member and by each entry's own age; every entry that leaves the queue without being delivered is reported to its sender as a terminal delivery status. */
|
|
23
31
|
pendingRoomRequests = new Map();
|
|
24
32
|
/** Pending room.join requests awaiting owner approval, keyed by `${roomPath}::${requesterId}` -- the held-open manage-request's own resolve() function is stored here so acceptRoomJoin/rejectRoomJoin can settle it in place. */
|
|
25
33
|
pendingRoomJoins = new Map();
|
|
@@ -199,7 +207,7 @@ export class RoomProtocol {
|
|
|
199
207
|
type: "delivery_status",
|
|
200
208
|
messageId,
|
|
201
209
|
agent: handle.id,
|
|
202
|
-
status: "read",
|
|
210
|
+
delivery: { status: "read" },
|
|
203
211
|
...(isDm ? {} : { room: roomPath }),
|
|
204
212
|
};
|
|
205
213
|
this.deps.deliveryEngine.queueDelivery(peerId, event);
|
|
@@ -266,20 +274,59 @@ export class RoomProtocol {
|
|
|
266
274
|
throw new CommsError(`room.send to ${memberId} for ${roomPath} failed (${outcome.code})`, "SEND_FAILED");
|
|
267
275
|
}
|
|
268
276
|
}
|
|
269
|
-
/**
|
|
270
|
-
|
|
277
|
+
/** Tells this device's own agent what became of a message it sent, over the same delivery_status event read receipts already ride, so a queued, dropped, expired or refused message reaches the sender through the one path that already reports delivery. Does nothing for a request carrying no message of this sender's own (a room.read receipt, a room.notify), which has no message-id to report against. */
|
|
278
|
+
reportDelivery(memberId, entry, delivery) {
|
|
279
|
+
if (entry.messageId === undefined)
|
|
280
|
+
return;
|
|
281
|
+
const peerId = this.deps.getPeerId();
|
|
282
|
+
const isDm = parseRoomPath(entry.roomPath).kind === "dm";
|
|
283
|
+
const event = {
|
|
284
|
+
type: "delivery_status",
|
|
285
|
+
messageId: entry.messageId,
|
|
286
|
+
agent: memberId,
|
|
287
|
+
delivery,
|
|
288
|
+
...(isDm ? {} : { room: entry.roomPath }),
|
|
289
|
+
};
|
|
290
|
+
this.deps.deliveryEngine.queueDelivery(peerId, event);
|
|
291
|
+
this.deps.deliveryEngine.fireLocalDelivery(peerId, event);
|
|
292
|
+
}
|
|
293
|
+
/** Holds a request for retry, evicting the oldest entries beyond the per-member bound and telling each evicted entry's sender the message was dropped, so a message given up on is never left looking merely pending. */
|
|
294
|
+
enqueuePendingRoomRequest(memberId, entry) {
|
|
271
295
|
const queue = this.pendingRoomRequests.get(memberId) ?? [];
|
|
272
|
-
queue.push(
|
|
273
|
-
|
|
274
|
-
queue.
|
|
296
|
+
queue.push(entry);
|
|
297
|
+
while (queue.length > MAX_PENDING_ROOM_REQUESTS_PER_MEMBER) {
|
|
298
|
+
const evicted = queue.shift();
|
|
299
|
+
if (evicted !== undefined) {
|
|
300
|
+
this.reportDelivery(memberId, evicted, { status: "dropped" });
|
|
301
|
+
}
|
|
275
302
|
}
|
|
276
303
|
this.pendingRoomRequests.set(memberId, queue);
|
|
277
304
|
}
|
|
278
305
|
/**
|
|
279
|
-
*
|
|
306
|
+
* Gives up on every queued request older than the age bound, across every member, telling each one's sender it expired. Deliberately a sweep run from sendRoomRequestToMember and flushPendingRoomRequests rather than a timer: the queue only ever grows on a send and only ever drains on a flush, so those are exactly the moments an expired entry starts to matter, and nothing has to keep a timer alive for a member that may never come back.
|
|
280
307
|
*/
|
|
281
|
-
|
|
282
|
-
//
|
|
308
|
+
expirePendingRoomRequests() {
|
|
309
|
+
// Nothing queued means nothing to sweep, and nothing to ask this store's identity for: a bridge with no identity yet still sees peers connect, and a sweep must not turn that into a rejected promise inside a fire-and-forget event handler.
|
|
310
|
+
if (this.pendingRoomRequests.size === 0)
|
|
311
|
+
return;
|
|
312
|
+
const cutoff = this.deps.requireIdentity().clock.now() - PENDING_ROOM_REQUEST_TTL_MS;
|
|
313
|
+
for (const [memberId, queue] of this.pendingRoomRequests) {
|
|
314
|
+
const live = queue.filter((entry) => entry.queuedAt > cutoff);
|
|
315
|
+
if (live.length === queue.length)
|
|
316
|
+
continue;
|
|
317
|
+
for (const entry of queue) {
|
|
318
|
+
if (entry.queuedAt <= cutoff) {
|
|
319
|
+
this.reportDelivery(memberId, entry, { status: "expired" });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (live.length === 0)
|
|
323
|
+
this.pendingRoomRequests.delete(memberId);
|
|
324
|
+
else
|
|
325
|
+
this.pendingRoomRequests.set(memberId, live);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** One attempt at a directed room-domain request, with no queueing of its own: the raw wire outcome classified into delivered, undelivered (nobody answered, so a retry may still work) or refused (the member answered with a failure it will repeat). A rejection from the transport is the connection dropping mid-request, per wire-mesh-core's own rejectPendingManageRequests, which is the same "never answered" fact as a timeout. */
|
|
329
|
+
async attemptRoomRequest(memberId, roomPath, token, params) {
|
|
283
330
|
let outcome;
|
|
284
331
|
try {
|
|
285
332
|
outcome = await this.deps
|
|
@@ -287,15 +334,32 @@ export class RoomProtocol {
|
|
|
287
334
|
.sendRoomRequest(memberId, { verb: ROOM_MEMBER_CAPABILITY, params }, { kind: "room", path: roomPath }, token);
|
|
288
335
|
}
|
|
289
336
|
catch {
|
|
290
|
-
|
|
291
|
-
return;
|
|
337
|
+
return { kind: "undelivered", reason: "connection_lost" };
|
|
292
338
|
}
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
return classifyManageOutcome(outcome);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Sends one directed room-domain request (room.send, room.read, or any future room:member-gated verb) to a single member and reports honestly what became of it -- the fan-out's own per-recipient primitive, distinct from sendRoomMessageDirected's deliberate throw-on-failure contract for a caller sending to one specific, known recipient. An undelivered request is held for retry and says so; a refused one is not queued at all, since the member has already answered with a failure it will give again, and its sender needs to know now rather than waiting on a retry that cannot help.
|
|
343
|
+
*/
|
|
344
|
+
async sendRoomRequestToMember(memberId, roomPath, token, params) {
|
|
345
|
+
this.expirePendingRoomRequests();
|
|
346
|
+
const outcome = await this.attemptRoomRequest(memberId, roomPath, token, params);
|
|
347
|
+
if (outcome.kind === "undelivered") {
|
|
348
|
+
this.enqueuePendingRoomRequest(memberId, {
|
|
349
|
+
roomPath,
|
|
350
|
+
params,
|
|
351
|
+
queuedAt: this.deps.requireIdentity().clock.now(),
|
|
352
|
+
reason: outcome.reason,
|
|
353
|
+
messageId: messageIdFromParams(params),
|
|
354
|
+
});
|
|
295
355
|
}
|
|
356
|
+
return outcome;
|
|
296
357
|
}
|
|
297
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* Retries every request queued for memberId since it was last reachable, and tells each one's sender what came of the retry: delivered, refused, or still undelivered and held for another attempt. An entry whose room this store no longer holds a token for can never be sent at all, so it is reported as dropped rather than retried forever. Called whenever memberId becomes reachable by any route -- a direct peer connection, or the hub admitting its device into this side's directory -- since a queued request cares only that some path to the member now exists, not which one.
|
|
360
|
+
*/
|
|
298
361
|
async flushPendingRoomRequests(memberId) {
|
|
362
|
+
this.expirePendingRoomRequests();
|
|
299
363
|
const queue = this.pendingRoomRequests.get(memberId);
|
|
300
364
|
if (queue === undefined || queue.length === 0)
|
|
301
365
|
return;
|
|
@@ -303,9 +367,27 @@ export class RoomProtocol {
|
|
|
303
367
|
const { slot } = this.deps.requireIdentity();
|
|
304
368
|
for (const pending of queue) {
|
|
305
369
|
const token = loadRoomTokens(slot)[pending.roomPath];
|
|
306
|
-
if (token === undefined)
|
|
370
|
+
if (token === undefined) {
|
|
371
|
+
this.reportDelivery(memberId, pending, { status: "dropped" });
|
|
307
372
|
continue;
|
|
308
|
-
|
|
373
|
+
}
|
|
374
|
+
const outcome = await this.attemptRoomRequest(memberId, pending.roomPath, token, pending.params);
|
|
375
|
+
if (outcome.kind === "delivered") {
|
|
376
|
+
this.reportDelivery(memberId, pending, { status: "delivered" });
|
|
377
|
+
}
|
|
378
|
+
else if (outcome.kind === "refused") {
|
|
379
|
+
this.reportDelivery(memberId, pending, {
|
|
380
|
+
status: "refused",
|
|
381
|
+
code: outcome.code,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Re-queued with its original queuedAt, never a fresh one: a member that keeps briefly reappearing and failing must not be able to keep a message pending past the age bound.
|
|
386
|
+
this.enqueuePendingRoomRequest(memberId, {
|
|
387
|
+
...pending,
|
|
388
|
+
reason: outcome.reason,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
309
391
|
}
|
|
310
392
|
}
|
|
311
393
|
/**
|