@openlap/openlap 1.1.2 → 1.1.4
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/dist/channel.d.ts +2 -1
- package/dist/channel.js +26 -30
- package/dist/proxy.js +17 -5
- package/package.json +1 -1
package/dist/channel.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ export declare class ChannelManager {
|
|
|
33
33
|
private onPresence;
|
|
34
34
|
private getToken;
|
|
35
35
|
private mySessionId;
|
|
36
|
-
private heartbeatInterval;
|
|
37
36
|
constructor(baseUrl: string, onUpdate: UpdateCallback, getToken: () => string | undefined, onPresence?: PresenceCallback);
|
|
38
37
|
setSessionId(sessionId: string): void;
|
|
39
38
|
join(channel: string, name?: string): Promise<JoinResponse | null>;
|
|
@@ -43,6 +42,8 @@ export declare class ChannelManager {
|
|
|
43
42
|
getFocusedChannel(): string | undefined;
|
|
44
43
|
isJoined(channel: string): boolean;
|
|
45
44
|
isMuted(channel: string): boolean;
|
|
45
|
+
hasChannels(): boolean;
|
|
46
|
+
heartbeat(): Promise<void>;
|
|
46
47
|
private connectSSE;
|
|
47
48
|
private readStream;
|
|
48
49
|
private handleSSEEvent;
|
package/dist/channel.js
CHANGED
|
@@ -9,7 +9,6 @@ export class ChannelManager {
|
|
|
9
9
|
onPresence;
|
|
10
10
|
getToken;
|
|
11
11
|
mySessionId;
|
|
12
|
-
heartbeatInterval = null;
|
|
13
12
|
constructor(baseUrl, onUpdate, getToken, onPresence) {
|
|
14
13
|
this.baseUrl = baseUrl;
|
|
15
14
|
this.onUpdate = onUpdate;
|
|
@@ -39,6 +38,10 @@ export class ChannelManager {
|
|
|
39
38
|
}
|
|
40
39
|
const data = (await res.json());
|
|
41
40
|
process.stderr.write(`[openlap] joined ${channel} (${data.present?.length ?? 0} present)\n`);
|
|
41
|
+
// Set session_id for echo suppression BEFORE opening SSE (fixes race condition)
|
|
42
|
+
if (data.session_id) {
|
|
43
|
+
this.mySessionId = data.session_id;
|
|
44
|
+
}
|
|
42
45
|
// Start SSE subscription
|
|
43
46
|
const controller = new AbortController();
|
|
44
47
|
this.channels.set(channel, { controller, muted: false, focused: false });
|
|
@@ -118,6 +121,24 @@ export class ChannelManager {
|
|
|
118
121
|
isMuted(channel) {
|
|
119
122
|
return this.channels.get(channel)?.muted ?? false;
|
|
120
123
|
}
|
|
124
|
+
hasChannels() {
|
|
125
|
+
return this.channels.size > 0;
|
|
126
|
+
}
|
|
127
|
+
// Public heartbeat -- called by proxy on every tool invocation.
|
|
128
|
+
async heartbeat() {
|
|
129
|
+
const token = this.getToken();
|
|
130
|
+
if (!token)
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
await fetch(`${this.baseUrl}/api/presence/heartbeat`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// not critical
|
|
140
|
+
}
|
|
141
|
+
}
|
|
121
142
|
// SSE connection for a channel
|
|
122
143
|
connectSSE(channel, controller) {
|
|
123
144
|
const url = `${this.baseUrl}/feed/${channel}/sse`;
|
|
@@ -221,38 +242,13 @@ export class ChannelManager {
|
|
|
221
242
|
}
|
|
222
243
|
}
|
|
223
244
|
}
|
|
224
|
-
//
|
|
245
|
+
// Immediate heartbeat on join. Ongoing heartbeats piggyback on tool calls
|
|
246
|
+
// (setInterval doesn't fire reliably in MCP stdio context).
|
|
225
247
|
startHeartbeat() {
|
|
226
|
-
|
|
227
|
-
return;
|
|
228
|
-
// Fire immediate heartbeat on first join
|
|
229
|
-
const doHeartbeat = async () => {
|
|
230
|
-
const token = this.getToken();
|
|
231
|
-
if (!token) {
|
|
232
|
-
process.stderr.write(`[openlap] heartbeat skipped: no token\n`);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
const res = await fetch(`${this.baseUrl}/api/presence/heartbeat`, {
|
|
237
|
-
method: "POST",
|
|
238
|
-
headers: { "Authorization": `Bearer ${token}` },
|
|
239
|
-
});
|
|
240
|
-
if (!res.ok) {
|
|
241
|
-
process.stderr.write(`[openlap] heartbeat failed: ${res.status}\n`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
process.stderr.write(`[openlap] heartbeat error: ${err}\n`);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
doHeartbeat(); // immediate first heartbeat
|
|
249
|
-
this.heartbeatInterval = setInterval(doHeartbeat, 30_000);
|
|
248
|
+
this.heartbeat().catch(() => { });
|
|
250
249
|
}
|
|
251
250
|
stopHeartbeat() {
|
|
252
|
-
|
|
253
|
-
clearInterval(this.heartbeatInterval);
|
|
254
|
-
this.heartbeatInterval = null;
|
|
255
|
-
}
|
|
251
|
+
// No interval to clear -- heartbeats piggyback on tool calls
|
|
256
252
|
}
|
|
257
253
|
stop() {
|
|
258
254
|
this.stopHeartbeat();
|
package/dist/proxy.js
CHANGED
|
@@ -174,17 +174,29 @@ export async function startProxy() {
|
|
|
174
174
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
175
175
|
const { name } = req.params;
|
|
176
176
|
const args = { ...(req.params.arguments ?? {}) };
|
|
177
|
+
// -- Heartbeat on every tool call (keeps presence alive) ----------------
|
|
178
|
+
if (channels.hasChannels()) {
|
|
179
|
+
channels.heartbeat().catch(() => { });
|
|
180
|
+
}
|
|
177
181
|
// -- v2 proxy-side channel tools (handled locally) ----------------------
|
|
178
182
|
if (name === "join_channel") {
|
|
179
183
|
const channel = args.channel;
|
|
180
184
|
if (!channel)
|
|
181
185
|
return { content: [{ type: "text", text: "channel required" }], isError: true };
|
|
182
186
|
const result = await channels.join(channel, args.name);
|
|
183
|
-
//
|
|
184
|
-
if (result
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
// session_id set internally by ChannelManager.join (before SSE opens)
|
|
188
|
+
if (!result)
|
|
189
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "already joined or failed" }) }] };
|
|
190
|
+
const r = result;
|
|
191
|
+
const lines = [`Joined ${channel}.`];
|
|
192
|
+
if (r.feed_url)
|
|
193
|
+
lines.push(`Feed: ${r.feed_url}`);
|
|
194
|
+
if (r.present?.length)
|
|
195
|
+
lines.push(`Present: ${r.present.join(", ")}`);
|
|
196
|
+
if (r.recent?.length)
|
|
197
|
+
lines.push(`Recent: ${r.recent.length} posts`);
|
|
198
|
+
lines.push("", JSON.stringify(result));
|
|
199
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
188
200
|
}
|
|
189
201
|
if (name === "leave_channel") {
|
|
190
202
|
const channel = args.channel;
|