@johpaz/hive 1.1.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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "bun:test";
|
|
2
|
+
import { CanvasManager, WebSocketState, type WebSocketLike } from "./canvas-manager.ts";
|
|
3
|
+
|
|
4
|
+
describe("CanvasManager", () => {
|
|
5
|
+
let manager: CanvasManager;
|
|
6
|
+
|
|
7
|
+
const createMockWebSocket = (): WebSocketLike & { messages: string[] } => {
|
|
8
|
+
const messages: string[] = [];
|
|
9
|
+
const handlers: Map<string, (data: unknown) => void> = new Map();
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
readyState: WebSocketState.OPEN,
|
|
13
|
+
send: (data: string) => {
|
|
14
|
+
messages.push(data);
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
on: (event: string, callback: (data: unknown) => void) => {
|
|
18
|
+
handlers.set(event, callback);
|
|
19
|
+
},
|
|
20
|
+
close: () => {},
|
|
21
|
+
messages,
|
|
22
|
+
} as unknown as WebSocketLike & { messages: string[] };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
manager = new CanvasManager();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("registerSession", () => {
|
|
30
|
+
it("should register a session", () => {
|
|
31
|
+
const ws = createMockWebSocket();
|
|
32
|
+
manager.registerSession("session-1", ws);
|
|
33
|
+
|
|
34
|
+
expect(manager.isSessionConnected("session-1")).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should track session stats", () => {
|
|
38
|
+
const ws = createMockWebSocket();
|
|
39
|
+
manager.registerSession("session-1", ws);
|
|
40
|
+
|
|
41
|
+
const stats = manager.getStats();
|
|
42
|
+
expect(stats.totalSessions).toBe(1);
|
|
43
|
+
expect(stats.activeSessions).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("render", () => {
|
|
48
|
+
it("should send render message to session", async () => {
|
|
49
|
+
const ws = createMockWebSocket();
|
|
50
|
+
manager.registerSession("session-1", ws);
|
|
51
|
+
|
|
52
|
+
const component = {
|
|
53
|
+
id: "btn-1",
|
|
54
|
+
type: "button" as const,
|
|
55
|
+
props: { label: "Click me" },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await manager.render("session-1", component);
|
|
59
|
+
|
|
60
|
+
expect(ws.messages.length).toBe(1);
|
|
61
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
62
|
+
expect(msg.type).toBe("canvas:render");
|
|
63
|
+
expect(msg.payload.component.id).toBe("btn-1");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should throw for disconnected session", async () => {
|
|
67
|
+
await expect(manager.render("unknown", { id: "x", type: "button", props: {} })).rejects.toThrow(
|
|
68
|
+
"Session not connected"
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("update", () => {
|
|
74
|
+
it("should send update message to session", async () => {
|
|
75
|
+
const ws = createMockWebSocket();
|
|
76
|
+
manager.registerSession("session-1", ws);
|
|
77
|
+
|
|
78
|
+
const component = {
|
|
79
|
+
id: "btn-1",
|
|
80
|
+
type: "button" as const,
|
|
81
|
+
props: { label: "Updated" },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await manager.update("session-1", component);
|
|
85
|
+
|
|
86
|
+
expect(ws.messages.length).toBe(1);
|
|
87
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
88
|
+
expect(msg.type).toBe("canvas:update");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("clear", () => {
|
|
93
|
+
it("should send clear message to session", async () => {
|
|
94
|
+
const ws = createMockWebSocket();
|
|
95
|
+
manager.registerSession("session-1", ws);
|
|
96
|
+
|
|
97
|
+
await manager.clear("session-1");
|
|
98
|
+
|
|
99
|
+
expect(ws.messages.length).toBe(1);
|
|
100
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
101
|
+
expect(msg.type).toBe("canvas:clear");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("waitForInteraction", () => {
|
|
106
|
+
it("should resolve when interaction is received", async () => {
|
|
107
|
+
const ws = createMockWebSocket();
|
|
108
|
+
manager.registerSession("session-1", ws);
|
|
109
|
+
|
|
110
|
+
const component = { id: "form-1", type: "form" as const, props: {} };
|
|
111
|
+
await manager.render("session-1", component);
|
|
112
|
+
|
|
113
|
+
const interactionPromise = manager.waitForInteraction("session-1", "form-1", 5000);
|
|
114
|
+
|
|
115
|
+
manager.handleInteraction("session-1", "form-1", { name: "John" });
|
|
116
|
+
|
|
117
|
+
const result = await interactionPromise;
|
|
118
|
+
expect(result).toEqual({ name: "John" });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("getStats", () => {
|
|
123
|
+
it("should return correct stats", () => {
|
|
124
|
+
const ws1 = createMockWebSocket();
|
|
125
|
+
const ws2 = createMockWebSocket();
|
|
126
|
+
|
|
127
|
+
manager.registerSession("session-1", ws1);
|
|
128
|
+
manager.registerSession("session-2", ws2);
|
|
129
|
+
|
|
130
|
+
const stats = manager.getStats();
|
|
131
|
+
expect(stats.totalSessions).toBe(2);
|
|
132
|
+
expect(stats.activeSessions).toBe(2);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("getConnectedSessions", () => {
|
|
137
|
+
it("should list connected sessions", () => {
|
|
138
|
+
const ws1 = createMockWebSocket();
|
|
139
|
+
const ws2 = createMockWebSocket();
|
|
140
|
+
|
|
141
|
+
manager.registerSession("session-1", ws1);
|
|
142
|
+
manager.registerSession("session-2", ws2);
|
|
143
|
+
|
|
144
|
+
const sessions = manager.getConnectedSessions();
|
|
145
|
+
expect(sessions).toContain("session-1");
|
|
146
|
+
expect(sessions).toContain("session-2");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("clearAll", () => {
|
|
151
|
+
it("should clear all sessions and pending interactions", () => {
|
|
152
|
+
const ws = createMockWebSocket();
|
|
153
|
+
manager.registerSession("session-1", ws);
|
|
154
|
+
|
|
155
|
+
manager.clearAll();
|
|
156
|
+
|
|
157
|
+
const stats = manager.getStats();
|
|
158
|
+
expect(stats.totalSessions).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
4
|
+
|
|
5
|
+
export interface WebSocketLike {
|
|
6
|
+
readyState: number;
|
|
7
|
+
send(data: string | ArrayBuffer | Uint8Array): number | boolean;
|
|
8
|
+
close(code?: number, reason?: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const WebSocketState = {
|
|
12
|
+
CONNECTING: 0,
|
|
13
|
+
OPEN: 1,
|
|
14
|
+
CLOSING: 2,
|
|
15
|
+
CLOSED: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface CanvasComponent {
|
|
19
|
+
id: string;
|
|
20
|
+
type: "button" | "form" | "chart" | "table" | "markdown" | "text" | "image" | "card" | "progress" | "list" | "confirm";
|
|
21
|
+
props: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CanvasMessage {
|
|
25
|
+
type: "canvas:render" | "canvas:update" | "canvas:clear" | "canvas:interact" | "canvas:connected" | "canvas:snapshot";
|
|
26
|
+
sessionId: string;
|
|
27
|
+
componentId?: string;
|
|
28
|
+
component?: CanvasComponent;
|
|
29
|
+
action?: string;
|
|
30
|
+
data?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InteractionEvent {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
componentId: string;
|
|
36
|
+
action: string;
|
|
37
|
+
data: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PendingInteraction {
|
|
41
|
+
resolve: (data: unknown) => void;
|
|
42
|
+
reject: (error: Error) => void;
|
|
43
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class CanvasManager extends EventEmitter {
|
|
47
|
+
private sessions: Map<string, WebSocketLike> = new Map();
|
|
48
|
+
private pendingInteractions: Map<string, PendingInteraction> = new Map();
|
|
49
|
+
private componentCache: Map<string, CanvasComponent[]> = new Map();
|
|
50
|
+
private log = logger.child("canvas");
|
|
51
|
+
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
super();
|
|
55
|
+
this.startHeartbeat();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private startHeartbeat(): void {
|
|
59
|
+
if (this.heartbeatInterval) return;
|
|
60
|
+
|
|
61
|
+
// Enviar ping a todas las sesiones cada 30 segundos
|
|
62
|
+
this.heartbeatInterval = setInterval(() => {
|
|
63
|
+
for (const [sessionId, ws] of this.sessions) {
|
|
64
|
+
if (ws.readyState === WebSocketState.OPEN) {
|
|
65
|
+
try {
|
|
66
|
+
ws.send(JSON.stringify({ type: "canvas:ping", sessionId }));
|
|
67
|
+
this.log.debug(`Heartbeat sent to ${sessionId}`);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
this.log.error(`Failed to send heartbeat to ${sessionId}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, 30000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private stopHeartbeat(): void {
|
|
77
|
+
if (this.heartbeatInterval) {
|
|
78
|
+
clearInterval(this.heartbeatInterval);
|
|
79
|
+
this.heartbeatInterval = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
registerSession(sessionId: string, ws: WebSocketLike): void {
|
|
84
|
+
this.sessions.set(sessionId, ws);
|
|
85
|
+
this.log.info(`Canvas session registered: ${sessionId}`);
|
|
86
|
+
|
|
87
|
+
eventBus.emit("tool:completed" as any, {
|
|
88
|
+
toolName: "canvas:session:register",
|
|
89
|
+
result: { sessionId },
|
|
90
|
+
duration: 0,
|
|
91
|
+
success: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Notify the client that the session is registered
|
|
95
|
+
ws.send(JSON.stringify({ type: "canvas:connected", sessionId }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
unregisterSession(sessionId: string): void {
|
|
99
|
+
this.sessions.delete(sessionId);
|
|
100
|
+
this.cleanupPendingInteractions(sessionId);
|
|
101
|
+
this.log.info(`Canvas session disconnected: ${sessionId}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handleMessage(sessionId: string, data: unknown): void {
|
|
105
|
+
try {
|
|
106
|
+
const msg = JSON.parse(data as string);
|
|
107
|
+
|
|
108
|
+
this.log.debug(`Received message from session ${sessionId}: ${msg.type}`);
|
|
109
|
+
|
|
110
|
+
// === HANDSHAKE INICIAL ===
|
|
111
|
+
if (msg.type === "canvas:handshake") {
|
|
112
|
+
this.log.debug(`Handshake received from ${sessionId}`, msg);
|
|
113
|
+
const ws = this.sessions.get(sessionId);
|
|
114
|
+
|
|
115
|
+
// Confirmar handshake
|
|
116
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
117
|
+
ws.send(JSON.stringify({
|
|
118
|
+
type: "canvas:handshake:ack",
|
|
119
|
+
sessionId,
|
|
120
|
+
serverSessionId: sessionId,
|
|
121
|
+
timestamp: Date.now()
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// === HEARTBEAT: PING DEL CLIENTE ===
|
|
128
|
+
if (msg.type === "canvas:ping") {
|
|
129
|
+
const ws = this.sessions.get(sessionId);
|
|
130
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
131
|
+
ws.send(JSON.stringify({ type: "canvas:pong", sessionId }));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// === PONG DEL CLIENTE (opcional) ===
|
|
137
|
+
if (msg.type === "canvas:pong") {
|
|
138
|
+
// El cliente respondió al ping del servidor - conexión viva
|
|
139
|
+
this.log.debug(`Pong received from ${sessionId}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (msg.type === "canvas:interact") {
|
|
144
|
+
const { componentId, action, data: interactionData } = msg.payload || msg;
|
|
145
|
+
|
|
146
|
+
if (componentId) {
|
|
147
|
+
this.resolveInteraction(sessionId, componentId, interactionData);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.emit("interaction", {
|
|
151
|
+
sessionId,
|
|
152
|
+
componentId,
|
|
153
|
+
action,
|
|
154
|
+
data: interactionData,
|
|
155
|
+
} as InteractionEvent);
|
|
156
|
+
} else if (msg.type === "canvas:get_cached") {
|
|
157
|
+
const components = this.getSessionComponents(sessionId);
|
|
158
|
+
const ws = this.sessions.get(sessionId);
|
|
159
|
+
|
|
160
|
+
this.log.info(`Sending ${components.length} cached components to session ${sessionId}`);
|
|
161
|
+
|
|
162
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
163
|
+
for (const component of components) {
|
|
164
|
+
ws.send(JSON.stringify({
|
|
165
|
+
type: "canvas:render",
|
|
166
|
+
sessionId,
|
|
167
|
+
component
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.log.error(`Invalid canvas message: ${(error as Error).message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private resolveInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
178
|
+
const key = `${sessionId}:${componentId}`;
|
|
179
|
+
const pending = this.pendingInteractions.get(key);
|
|
180
|
+
|
|
181
|
+
if (pending) {
|
|
182
|
+
clearTimeout(pending.timeoutId);
|
|
183
|
+
this.pendingInteractions.delete(key);
|
|
184
|
+
pending.resolve(data);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async render(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
189
|
+
// Always cache the component for later retrieval
|
|
190
|
+
const cached = this.componentCache.get(sessionId) || [];
|
|
191
|
+
const existingIdx = cached.findIndex(c => c.id === component.id);
|
|
192
|
+
if (existingIdx >= 0) {
|
|
193
|
+
cached[existingIdx] = component;
|
|
194
|
+
} else {
|
|
195
|
+
cached.push(component);
|
|
196
|
+
}
|
|
197
|
+
this.componentCache.set(sessionId, cached);
|
|
198
|
+
|
|
199
|
+
const ws = this.sessions.get(sessionId);
|
|
200
|
+
|
|
201
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
202
|
+
// Session not connected, but we cached the component
|
|
203
|
+
this.log.debug(`Session ${sessionId} NOT connected. Available: ${this.getConnectedSessions().join(", ")}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const message: CanvasMessage = {
|
|
208
|
+
type: "canvas:render",
|
|
209
|
+
sessionId,
|
|
210
|
+
component,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
this.log.debug(`Sending render to session ${sessionId}: ${component.id}`);
|
|
214
|
+
ws.send(JSON.stringify(message));
|
|
215
|
+
this.log.debug(`Rendered component ${component.id} to session ${sessionId}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async update(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
219
|
+
const ws = this.sessions.get(sessionId);
|
|
220
|
+
|
|
221
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
222
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const message: CanvasMessage = {
|
|
226
|
+
type: "canvas:update",
|
|
227
|
+
sessionId,
|
|
228
|
+
component,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
ws.send(JSON.stringify(message));
|
|
232
|
+
this.log.debug(`Updated component ${component.id} in session ${sessionId}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async clear(sessionId: string): Promise<void> {
|
|
236
|
+
const ws = this.sessions.get(sessionId);
|
|
237
|
+
|
|
238
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
239
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const message: CanvasMessage = {
|
|
243
|
+
type: "canvas:clear",
|
|
244
|
+
sessionId,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
ws.send(JSON.stringify(message));
|
|
248
|
+
this.log.debug(`Cleared canvas for session ${sessionId}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async waitForInteraction(
|
|
252
|
+
sessionId: string,
|
|
253
|
+
componentId: string,
|
|
254
|
+
timeout = 300000
|
|
255
|
+
): Promise<unknown> {
|
|
256
|
+
const key = `${sessionId}:${componentId}`;
|
|
257
|
+
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const timeoutId = setTimeout(() => {
|
|
260
|
+
this.pendingInteractions.delete(key);
|
|
261
|
+
reject(new Error(`Interaction timeout for ${componentId}`));
|
|
262
|
+
}, timeout);
|
|
263
|
+
|
|
264
|
+
this.pendingInteractions.set(key, { resolve, reject, timeoutId });
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
handleInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
269
|
+
this.resolveInteraction(sessionId, componentId, data);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getSessionComponents(sessionId: string): CanvasComponent[] {
|
|
273
|
+
return this.componentCache.get(sessionId) || [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
isSessionConnected(sessionId: string): boolean {
|
|
277
|
+
const ws = this.sessions.get(sessionId);
|
|
278
|
+
return ws !== undefined && ws.readyState === WebSocketState.OPEN;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getConnectedSessions(): string[] {
|
|
282
|
+
return Array.from(this.sessions.entries())
|
|
283
|
+
.filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
|
|
284
|
+
.map(([id]) => id);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getStats(): { totalSessions: number; activeSessions: number; pendingInteractions: number } {
|
|
288
|
+
return {
|
|
289
|
+
totalSessions: this.sessions.size,
|
|
290
|
+
activeSessions: this.getConnectedSessions().length,
|
|
291
|
+
pendingInteractions: this.pendingInteractions.size,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private cleanupPendingInteractions(sessionId: string): void {
|
|
296
|
+
for (const [key, pending] of this.pendingInteractions) {
|
|
297
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
298
|
+
clearTimeout(pending.timeoutId);
|
|
299
|
+
pending.reject(new Error(`Session disconnected: ${sessionId}`));
|
|
300
|
+
this.pendingInteractions.delete(key);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
clearAll(): void {
|
|
306
|
+
this.stopHeartbeat();
|
|
307
|
+
|
|
308
|
+
for (const pending of this.pendingInteractions.values()) {
|
|
309
|
+
clearTimeout(pending.timeoutId);
|
|
310
|
+
pending.reject(new Error("Canvas manager cleared"));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.pendingInteractions.clear();
|
|
314
|
+
this.sessions.clear();
|
|
315
|
+
this.log.info("Canvas manager cleared");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const canvasManager = new CanvasManager();
|