@cryptiklemur/lattice 1.38.0 → 1.40.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/client/src/components/sidebar/ProjectRail.tsx +1 -1
- package/client/src/components/ui/NodeDisconnectedOverlay.tsx +35 -0
- package/client/src/hooks/useProjects.ts +21 -5
- package/client/src/router.tsx +3 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -1
- package/server/src/handlers/mesh.ts +6 -0
- package/server/src/index.ts +3 -0
- package/server/src/logger.ts +9 -0
- package/server/src/mesh/connector.ts +42 -15
- package/server/src/mesh/proxy.ts +9 -3
- package/server/src/ws/broadcast.ts +5 -0
- package/server/src/ws/router.ts +8 -1
- package/shared/src/models.ts +1 -0
|
@@ -31,7 +31,7 @@ function groupProjectsBySlug(projects: ProjectInfo[], nodes: NodeInfo[]): Projec
|
|
|
31
31
|
var nodeEntry = {
|
|
32
32
|
nodeId: p.nodeId,
|
|
33
33
|
nodeName: p.nodeName,
|
|
34
|
-
online: node ? node.online :
|
|
34
|
+
online: node ? node.online : (p.online ?? !p.isRemote),
|
|
35
35
|
path: p.path,
|
|
36
36
|
};
|
|
37
37
|
if (existing) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WifiOff, Loader2 } from "lucide-react";
|
|
2
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
3
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
4
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
5
|
+
|
|
6
|
+
export function NodeDisconnectedOverlay() {
|
|
7
|
+
var { activeProject } = useProjects();
|
|
8
|
+
var { nodes } = useMesh();
|
|
9
|
+
var sidebar = useSidebar();
|
|
10
|
+
|
|
11
|
+
if (!activeProject || !activeProject.isRemote) return null;
|
|
12
|
+
if (sidebar.activeView.type !== "chat") return null;
|
|
13
|
+
|
|
14
|
+
var remoteNode = nodes.find(function (n) { return n.id === activeProject!.nodeId; });
|
|
15
|
+
if (!remoteNode || remoteNode.online) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="absolute inset-0 z-40 flex items-center justify-center bg-base-100/80 backdrop-blur-sm">
|
|
19
|
+
<div className="bg-base-300 border border-base-content/15 rounded-2xl shadow-2xl px-8 py-6 max-w-sm text-center">
|
|
20
|
+
<WifiOff size={28} className="text-warning mx-auto mb-3" />
|
|
21
|
+
<h3 className="text-[15px] font-mono font-bold text-base-content mb-1">Node Disconnected</h3>
|
|
22
|
+
<p className="text-[13px] text-base-content/50 mb-3">
|
|
23
|
+
<span className="font-semibold text-base-content/70">{remoteNode.name}</span> is unreachable.
|
|
24
|
+
</p>
|
|
25
|
+
<div className="flex items-center justify-center gap-2 text-[12px] text-base-content/40">
|
|
26
|
+
<Loader2 size={12} className="animate-spin" />
|
|
27
|
+
Attempting to reconnect...
|
|
28
|
+
</div>
|
|
29
|
+
<p className="text-[11px] text-base-content/25 mt-3">
|
|
30
|
+
Session state preserved. Operations will resume on reconnect.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -22,14 +22,30 @@ export function useProjects(): UseProjectsResult {
|
|
|
22
22
|
useEffect(function () {
|
|
23
23
|
handleRef.current = function (msg: ServerMessage) {
|
|
24
24
|
if (msg.type === "projects:list") {
|
|
25
|
-
var
|
|
26
|
-
setProjects(
|
|
25
|
+
var incoming = (msg as ProjectsListMessage).projects;
|
|
26
|
+
setProjects(function (prev) {
|
|
27
|
+
var incomingKeys = new Set(incoming.map(function (p) { return p.slug + "@" + p.nodeId; }));
|
|
28
|
+
var kept = prev.filter(function (p) {
|
|
29
|
+
if (!p.isRemote) return false;
|
|
30
|
+
return !incomingKeys.has(p.slug + "@" + p.nodeId);
|
|
31
|
+
});
|
|
32
|
+
for (var i = 0; i < kept.length; i++) {
|
|
33
|
+
(kept[i] as any).online = false;
|
|
34
|
+
}
|
|
35
|
+
return incoming.concat(kept);
|
|
36
|
+
});
|
|
27
37
|
var storeState = getSidebarStore().state;
|
|
28
38
|
var currentSlug = storeState.activeProjectSlug;
|
|
29
39
|
if (currentSlug !== null) {
|
|
30
|
-
var found =
|
|
31
|
-
if (!found
|
|
32
|
-
|
|
40
|
+
var found = incoming.find(function (p: typeof incoming[number]) { return p.slug === currentSlug; });
|
|
41
|
+
if (!found) {
|
|
42
|
+
setProjects(function (current) {
|
|
43
|
+
var stillExists = current.find(function (p) { return p.slug === currentSlug; });
|
|
44
|
+
if (!stillExists && current.length > 0) {
|
|
45
|
+
setActiveProjectSlug(current[0].slug);
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
});
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
}
|
package/client/src/router.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { useSidebar } from "./hooks/useSidebar";
|
|
|
16
16
|
import { useWorkspace } from "./hooks/useWorkspace";
|
|
17
17
|
import { useWebSocket } from "./hooks/useWebSocket";
|
|
18
18
|
import { UpdateBanner } from "./components/ui/UpdateBanner";
|
|
19
|
+
import { NodeDisconnectedOverlay } from "./components/ui/NodeDisconnectedOverlay";
|
|
19
20
|
import { useSwipeDrawer } from "./hooks/useSwipeDrawer";
|
|
20
21
|
import { exitSettings, getSidebarStore, handlePopState, closeDrawer, toggleDrawer } from "./stores/sidebar";
|
|
21
22
|
|
|
@@ -422,9 +423,10 @@ function RootLayout() {
|
|
|
422
423
|
readOnly
|
|
423
424
|
/>
|
|
424
425
|
|
|
425
|
-
<main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
|
|
426
|
+
<main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden relative">
|
|
426
427
|
<UpdateBanner />
|
|
427
428
|
<Outlet />
|
|
429
|
+
<NodeDisconnectedOverlay />
|
|
428
430
|
</main>
|
|
429
431
|
|
|
430
432
|
<div ref={drawerSideRef} className="drawer-side z-50 h-full">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
package/server/src/daemon.ts
CHANGED
|
@@ -401,7 +401,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
401
401
|
var remoteProjects = getAllRemoteProjects(currentIdentity.id);
|
|
402
402
|
broadcast({
|
|
403
403
|
type: "projects:list",
|
|
404
|
-
projects: localProjects.concat(remoteProjects as typeof localProjects),
|
|
404
|
+
projects: localProjects.concat(remoteProjects as unknown as typeof localProjects),
|
|
405
405
|
});
|
|
406
406
|
var updateInfo = getCachedUpdateInfo();
|
|
407
407
|
if (updateInfo && updateInfo.updateAvailable) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientMessage, MeshPairMessage, MeshUnpairMessage, NodeInfo } from "@lattice/shared";
|
|
2
|
+
import { log } from "../logger";
|
|
2
3
|
import { registerHandler } from "../ws/router";
|
|
3
4
|
import { sendTo, broadcast } from "../ws/broadcast";
|
|
4
5
|
import { loadConfig } from "../config";
|
|
@@ -122,6 +123,8 @@ export function buildNodesMessage(): NodeInfo[] {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
126
|
+
log.meshHello("mesh message: %s from %s", (message as any).type, clientId.slice(0, 8));
|
|
127
|
+
|
|
125
128
|
if (message.type === "mesh:generate_invite") {
|
|
126
129
|
var genMsg = message as any as { type: "mesh:generate_invite"; address?: string };
|
|
127
130
|
var config = loadConfig();
|
|
@@ -232,14 +235,17 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
232
235
|
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; publicKey?: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
|
|
233
236
|
|
|
234
237
|
var knownPeer = hello.nodeId ? getPeer(hello.nodeId) : undefined;
|
|
238
|
+
log.meshHello("mesh:hello from nodeId=%s name=%s known=%s", hello.nodeId?.slice(0, 8), hello.name, !!knownPeer);
|
|
235
239
|
|
|
236
240
|
if (knownPeer) {
|
|
237
241
|
if (knownPeer.publicKey && hello.publicKey && knownPeer.publicKey !== hello.publicKey) {
|
|
242
|
+
log.meshHello(" ✗ public key mismatch for %s", hello.name);
|
|
238
243
|
sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Public key mismatch — possible impersonation" });
|
|
239
244
|
return;
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
var inboundWs = getClientWebSocket(clientId);
|
|
248
|
+
log.meshHello(" registering inbound connection for %s (ws=%s)", hello.name, !!inboundWs);
|
|
243
249
|
if (inboundWs) {
|
|
244
250
|
registerInboundPeer(hello.nodeId, inboundWs as any);
|
|
245
251
|
}
|
package/server/src/index.ts
CHANGED
|
@@ -224,6 +224,9 @@ function runHelp(): void {
|
|
|
224
224
|
console.log(" Environment:");
|
|
225
225
|
console.log(" LATTICE_HOME Data directory (default: ~/.lattice)");
|
|
226
226
|
console.log(" LATTICE_PORT Server port (default: 7654)");
|
|
227
|
+
console.log(" DEBUG Enable debug logging (e.g. DEBUG=lattice:*)");
|
|
228
|
+
console.log(" Scopes: server,ws,router,mesh,mesh:connect,mesh:hello,");
|
|
229
|
+
console.log(" mesh:proxy,broadcast,chat,session,plugins,update");
|
|
227
230
|
console.log("");
|
|
228
231
|
}
|
|
229
232
|
|
package/server/src/logger.ts
CHANGED
|
@@ -6,7 +6,16 @@ export var log = {
|
|
|
6
6
|
chat: createDebug("lattice:chat"),
|
|
7
7
|
session: createDebug("lattice:session"),
|
|
8
8
|
mesh: createDebug("lattice:mesh"),
|
|
9
|
+
meshConnect: createDebug("lattice:mesh:connect"),
|
|
10
|
+
meshHello: createDebug("lattice:mesh:hello"),
|
|
11
|
+
meshProxy: createDebug("lattice:mesh:proxy"),
|
|
12
|
+
router: createDebug("lattice:router"),
|
|
13
|
+
broadcast: createDebug("lattice:broadcast"),
|
|
9
14
|
auth: createDebug("lattice:auth"),
|
|
10
15
|
fs: createDebug("lattice:fs"),
|
|
11
16
|
analytics: createDebug("lattice:analytics"),
|
|
17
|
+
plugins: createDebug("lattice:plugins"),
|
|
18
|
+
update: createDebug("lattice:update"),
|
|
19
|
+
terminal: createDebug("lattice:terminal"),
|
|
20
|
+
settings: createDebug("lattice:settings"),
|
|
12
21
|
};
|
|
@@ -17,6 +17,7 @@ interface PeerConnection {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
var connections = new Map<string, PeerConnection>();
|
|
20
|
+
var lastKnownProjects = new Map<string, Array<{ slug: string; title: string }>>();
|
|
20
21
|
var connectedCallbacks: Array<(nodeId: string) => void> = [];
|
|
21
22
|
var disconnectedCallbacks: Array<(nodeId: string) => void> = [];
|
|
22
23
|
var messageCallbacks: Array<(nodeId: string, msg: MeshMessage) => void> = [];
|
|
@@ -46,11 +47,18 @@ function reconcilePeers(): void {
|
|
|
46
47
|
var peers = loadPeers();
|
|
47
48
|
for (var i = 0; i < peers.length; i++) {
|
|
48
49
|
var peer = peers[i];
|
|
49
|
-
if (!peer.addresses || peer.addresses.length === 0)
|
|
50
|
+
if (!peer.addresses || peer.addresses.length === 0) {
|
|
51
|
+
log.meshConnect("skip %s — no addresses", peer.name);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
50
54
|
var existing = connections.get(peer.id);
|
|
51
55
|
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) continue;
|
|
52
|
-
if (existing && !existing.dead && existing.retryTimer !== null)
|
|
56
|
+
if (existing && !existing.dead && existing.retryTimer !== null) {
|
|
57
|
+
log.meshConnect("skip %s — retry pending", peer.name);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
53
60
|
if (!existing || existing.dead) {
|
|
61
|
+
log.meshConnect("connecting to %s at %s", peer.name, peer.addresses[0]);
|
|
54
62
|
connections.delete(peer.id);
|
|
55
63
|
connectToPeer(peer.id, peer.addresses[0]);
|
|
56
64
|
}
|
|
@@ -103,6 +111,7 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
103
111
|
var circuit = circuitBreakers.get(conn.nodeId);
|
|
104
112
|
if (circuit && circuit.failures >= CIRCUIT_BREAKER_THRESHOLD && !circuit.halfOpen) {
|
|
105
113
|
if (Date.now() < circuit.openUntil) {
|
|
114
|
+
log.meshConnect("circuit breaker open for %s, retry in %dms", conn.nodeId.slice(0, 8), circuit.openUntil - Date.now());
|
|
106
115
|
conn.retryTimer = setTimeout(function () {
|
|
107
116
|
if (conn.dead) return;
|
|
108
117
|
conn.retryTimer = null;
|
|
@@ -114,12 +123,13 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
114
123
|
circuit.halfOpen = true;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
log.meshConnect("opening WebSocket to %s", url);
|
|
117
127
|
var ws = new WebSocket(url);
|
|
118
128
|
conn.ws = ws;
|
|
119
129
|
|
|
120
130
|
var connectionTimer = setTimeout(function () {
|
|
121
131
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
122
|
-
log.
|
|
132
|
+
log.meshConnect("connection timeout for %s at %s", conn.nodeId.slice(0, 8), url);
|
|
123
133
|
ws.close();
|
|
124
134
|
}
|
|
125
135
|
}, CONNECTION_TIMEOUT_MS);
|
|
@@ -167,6 +177,9 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
167
177
|
|
|
168
178
|
if (msg.type === "mesh:hello") {
|
|
169
179
|
conn.projects = msg.projects;
|
|
180
|
+
if (msg.projects.length > 0) {
|
|
181
|
+
lastKnownProjects.set(conn.nodeId, msg.projects);
|
|
182
|
+
}
|
|
170
183
|
} else if (msg.type === "mesh:session_sync") {
|
|
171
184
|
handleSessionSync(conn.nodeId, msg as MeshSessionSyncMessage);
|
|
172
185
|
} else if (msg.type === "mesh:session_request") {
|
|
@@ -242,8 +255,10 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
|
|
|
242
255
|
export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }): void {
|
|
243
256
|
var existing = connections.get(nodeId);
|
|
244
257
|
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) {
|
|
258
|
+
log.meshConnect("inbound peer %s already connected, skipping", nodeId.slice(0, 8));
|
|
245
259
|
return;
|
|
246
260
|
}
|
|
261
|
+
log.meshConnect("registering inbound peer %s", nodeId.slice(0, 8));
|
|
247
262
|
|
|
248
263
|
if (existing) {
|
|
249
264
|
existing.dead = true;
|
|
@@ -347,21 +362,33 @@ export function getConnectedPeerProjects(nodeId: string): Array<{ slug: string;
|
|
|
347
362
|
return conn.projects;
|
|
348
363
|
}
|
|
349
364
|
|
|
350
|
-
export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> {
|
|
351
|
-
var
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
365
|
+
export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> {
|
|
366
|
+
var peersModule = require("./peers") as typeof import("./peers");
|
|
367
|
+
var allPeers = peersModule.loadPeers();
|
|
368
|
+
var connectedIds = new Set(getConnectedPeerIds());
|
|
369
|
+
var results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> = [];
|
|
370
|
+
|
|
371
|
+
for (var p = 0; p < allPeers.length; p++) {
|
|
372
|
+
var peer = allPeers[p];
|
|
373
|
+
var isOnline = connectedIds.has(peer.id);
|
|
374
|
+
var projects: Array<{ slug: string; title: string }> = [];
|
|
375
|
+
|
|
376
|
+
var conn = connections.get(peer.id);
|
|
377
|
+
if (conn && conn.projects.length > 0) {
|
|
378
|
+
projects = conn.projects;
|
|
379
|
+
} else if (lastKnownProjects.has(peer.id)) {
|
|
380
|
+
projects = lastKnownProjects.get(peer.id)!;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (var i = 0; i < projects.length; i++) {
|
|
358
384
|
results.push({
|
|
359
|
-
slug:
|
|
385
|
+
slug: projects[i].slug,
|
|
360
386
|
path: "",
|
|
361
|
-
title:
|
|
362
|
-
nodeId:
|
|
363
|
-
nodeName:
|
|
387
|
+
title: projects[i].title,
|
|
388
|
+
nodeId: peer.id,
|
|
389
|
+
nodeName: peer.name,
|
|
364
390
|
isRemote: true,
|
|
391
|
+
online: isOnline,
|
|
365
392
|
});
|
|
366
393
|
}
|
|
367
394
|
}
|
package/server/src/mesh/proxy.ts
CHANGED
|
@@ -3,19 +3,22 @@ import type { ClientMessage, MeshProxyRequestMessage, MeshProxyResponseMessage,
|
|
|
3
3
|
import { getPeerConnection } from "./connector";
|
|
4
4
|
import { sendTo, broadcast, registerVirtualClient, removeVirtualClient } from "../ws/broadcast";
|
|
5
5
|
import { routeMessage } from "../ws/router";
|
|
6
|
+
import { log } from "../logger";
|
|
6
7
|
|
|
7
8
|
var pendingRequests = new Map<string, string>();
|
|
8
9
|
|
|
9
10
|
export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId: string, message: ClientMessage): void {
|
|
11
|
+
log.meshProxy("→ proxy %s to node %s for project %s", (message as any).type, nodeId.slice(0, 8), projectSlug);
|
|
10
12
|
var ws = getPeerConnection(nodeId);
|
|
11
13
|
if (!ws) {
|
|
12
|
-
|
|
13
|
-
sendTo(clientId, { type: "chat:error", message: "Remote node
|
|
14
|
+
log.meshProxy(" ✗ no connection to node %s", nodeId.slice(0, 8));
|
|
15
|
+
sendTo(clientId, { type: "chat:error", message: "Remote node is not connected" });
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
var requestId = randomUUID();
|
|
18
20
|
pendingRequests.set(requestId, clientId);
|
|
21
|
+
log.meshProxy(" envelope requestId=%s", requestId.slice(0, 8));
|
|
19
22
|
|
|
20
23
|
var envelope: MeshProxyRequestMessage = {
|
|
21
24
|
type: "mesh:proxy_request",
|
|
@@ -29,8 +32,10 @@ export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId:
|
|
|
29
32
|
|
|
30
33
|
export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMessage): void {
|
|
31
34
|
var proxyClientId = "mesh-proxy:" + sourceNodeId + ":" + msg.requestId;
|
|
35
|
+
log.meshProxy("← proxy_request from %s: %s for %s (reqId=%s)", sourceNodeId.slice(0, 8), (msg.payload as any).type, msg.projectSlug, msg.requestId.slice(0, 8));
|
|
32
36
|
|
|
33
37
|
registerVirtualClient(proxyClientId, function (response: object) {
|
|
38
|
+
log.meshProxy(" → proxy_response %s back to %s", (response as any).type, sourceNodeId.slice(0, 8));
|
|
34
39
|
var ws = getPeerConnection(sourceNodeId);
|
|
35
40
|
if (!ws) {
|
|
36
41
|
console.warn("[mesh/proxy] Cannot send response, no connection to: " + sourceNodeId);
|
|
@@ -53,9 +58,10 @@ export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMe
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function handleProxyResponse(msg: MeshProxyResponseMessage): void {
|
|
61
|
+
log.meshProxy("← proxy_response %s (reqId=%s)", (msg.payload as any).type, msg.requestId.slice(0, 8));
|
|
56
62
|
var clientId = pendingRequests.get(msg.requestId);
|
|
57
63
|
if (!clientId) {
|
|
58
|
-
|
|
64
|
+
log.meshProxy(" ✗ no pending request for %s", msg.requestId.slice(0, 8));
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -37,6 +37,11 @@ export function sendTo(id: string, message: object): void {
|
|
|
37
37
|
var virtualHandler = virtualSendHandlers.get(id);
|
|
38
38
|
if (virtualHandler) {
|
|
39
39
|
virtualHandler(message);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (id.startsWith("mesh-proxy:")) {
|
|
43
|
+
var { log } = require("../logger");
|
|
44
|
+
log.broadcast(" ✗ sendTo %s but no virtual handler registered (msg=%s)", id.slice(0, 30), (message as any).type);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
|
package/server/src/ws/router.ts
CHANGED
|
@@ -53,6 +53,8 @@ export function getClientRemoteNode(clientId: string): { nodeId: string; project
|
|
|
53
53
|
export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
54
54
|
var prefix = message.type.split(":")[0];
|
|
55
55
|
|
|
56
|
+
log.router("→ %s from client %s (prefix=%s)", message.type, clientId.slice(0, 8), prefix);
|
|
57
|
+
|
|
56
58
|
if (PROXIED_PREFIXES.has(prefix)) {
|
|
57
59
|
var remote = clientRemoteNode.get(clientId);
|
|
58
60
|
|
|
@@ -60,17 +62,21 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
60
62
|
|
|
61
63
|
if (msgSlug) {
|
|
62
64
|
var localProject = getLocalProject(msgSlug);
|
|
65
|
+
log.router(" slug=%s local=%s", msgSlug, localProject);
|
|
63
66
|
if (!localProject) {
|
|
64
67
|
var remoteEntry = getRemoteNodeForProject(msgSlug);
|
|
65
68
|
if (remoteEntry) {
|
|
69
|
+
log.router(" → proxying to remote node %s for project %s", remoteEntry.nodeId.slice(0, 8), msgSlug);
|
|
66
70
|
setClientRemoteNode(clientId, remoteEntry.nodeId, msgSlug);
|
|
67
71
|
proxyMessage(clientId, remoteEntry.nodeId, msgSlug, message);
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
74
|
+
log.router(" ✗ no remote node found for slug %s", msgSlug);
|
|
70
75
|
} else if (message.type === "session:activate" || message.type === "session:list_request") {
|
|
71
76
|
clearClientRemoteNode(clientId);
|
|
72
77
|
}
|
|
73
78
|
} else if (remote) {
|
|
79
|
+
log.router(" → proxying via cached remote node %s", remote.nodeId.slice(0, 8));
|
|
74
80
|
proxyMessage(clientId, remote.nodeId, remote.projectSlug, message);
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
@@ -78,6 +84,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
78
84
|
|
|
79
85
|
var handler = handlers.get(prefix);
|
|
80
86
|
if (handler) {
|
|
87
|
+
log.router(" → dispatching to %s handler", prefix);
|
|
81
88
|
try {
|
|
82
89
|
var result = handler(clientId, message);
|
|
83
90
|
if (result && typeof result.then === "function") {
|
|
@@ -94,7 +101,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
94
101
|
}
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
|
-
log.
|
|
104
|
+
log.router(" ✗ no handler for %s", message.type);
|
|
98
105
|
sendTo(clientId, { type: "error", message: `Unknown message type: ${message.type}` });
|
|
99
106
|
}
|
|
100
107
|
|