@atlisp/mcp 1.2.0 → 1.3.1
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/README.md +2 -2
- package/dist/atlisp-mcp.js +2314 -0
- package/dist/cad-worker.js +221 -0
- package/package.json +11 -6
- package/src/atlisp-mcp.js +0 -630
- package/src/cad-worker.js +0 -498
- package/src/cad.js +0 -271
- package/src/config.js +0 -50
- package/src/constants.js +0 -23
- package/src/errors.js +0 -55
- package/src/handlers/cad-handlers.js +0 -110
- package/src/handlers/function-handlers.js +0 -107
- package/src/handlers/package-handlers.js +0 -66
- package/src/handlers/prompt-handlers.js +0 -268
- package/src/handlers/resource-handlers.js +0 -364
- package/src/logger.js +0 -30
- package/src/session-manager.js +0 -89
- package/src/session-transport.js +0 -89
- package/src/sse-session-manager.js +0 -133
- package/src/sse-session.js +0 -314
- package/src/subscription-manager.js +0 -52
package/src/session-manager.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
export class Session {
|
|
2
|
-
constructor(clientId) {
|
|
3
|
-
this.clientId = clientId;
|
|
4
|
-
this.subscriptions = new Set();
|
|
5
|
-
this.createdAt = Date.now();
|
|
6
|
-
this.connected = false;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class SessionManager {
|
|
11
|
-
#sessions = new Map();
|
|
12
|
-
|
|
13
|
-
createSession(clientId) {
|
|
14
|
-
let session = this.#sessions.get(clientId);
|
|
15
|
-
if (!session) {
|
|
16
|
-
session = new Session(clientId);
|
|
17
|
-
this.#sessions.set(clientId, session);
|
|
18
|
-
}
|
|
19
|
-
return session;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
getSession(clientId) {
|
|
23
|
-
return this.#sessions.get(clientId) || null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
hasSession(clientId) {
|
|
27
|
-
return this.#sessions.has(clientId);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
removeSession(clientId) {
|
|
31
|
-
return this.#sessions.delete(clientId);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
subscribe(clientId, uri) {
|
|
35
|
-
const session = this.#sessions.get(clientId);
|
|
36
|
-
if (session) {
|
|
37
|
-
session.subscriptions.add(uri);
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
unsubscribe(clientId, uri) {
|
|
44
|
-
const session = this.#sessions.get(clientId);
|
|
45
|
-
if (session) {
|
|
46
|
-
return session.subscriptions.delete(uri);
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getSubscriptions(clientId) {
|
|
52
|
-
const session = this.#sessions.get(clientId);
|
|
53
|
-
return session ? Array.from(session.subscriptions) : [];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
isSubscribed(clientId, uri) {
|
|
57
|
-
const session = this.#sessions.get(clientId);
|
|
58
|
-
return session ? session.subscriptions.has(uri) : false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
getAllSubscribedUris() {
|
|
62
|
-
const all = new Set();
|
|
63
|
-
for (const session of this.#sessions.values()) {
|
|
64
|
-
for (const uri of session.subscriptions) {
|
|
65
|
-
all.add(uri);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return all;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
list() {
|
|
72
|
-
return Array.from(this.#sessions.entries()).map(([id, s]) => ({
|
|
73
|
-
clientId: id,
|
|
74
|
-
subscriptions: Array.from(s.subscriptions),
|
|
75
|
-
createdAt: s.createdAt,
|
|
76
|
-
connected: s.connected
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
count() {
|
|
81
|
-
return this.#sessions.size;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
clear() {
|
|
85
|
-
this.#sessions.clear();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default SessionManager;
|
package/src/session-transport.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
|
|
3
|
-
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
4
|
-
|
|
5
|
-
export class SessionTransport {
|
|
6
|
-
constructor(sessionId) {
|
|
7
|
-
this.sessionId = sessionId || randomUUID();
|
|
8
|
-
this.onclose = null;
|
|
9
|
-
this.onerror = null;
|
|
10
|
-
this.onmessage = null;
|
|
11
|
-
this._pendingRequest = null;
|
|
12
|
-
this._started = false;
|
|
13
|
-
this._closed = false;
|
|
14
|
-
this._cleanupTimer = null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async start() {
|
|
18
|
-
if (this._started) {
|
|
19
|
-
throw new Error('Transport already started');
|
|
20
|
-
}
|
|
21
|
-
this._started = true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async send(message) {
|
|
25
|
-
if (this._closed) return;
|
|
26
|
-
if (this._pendingRequest) {
|
|
27
|
-
const { resolve, reject, timeout } = this._pendingRequest;
|
|
28
|
-
clearTimeout(timeout);
|
|
29
|
-
this._pendingRequest = null;
|
|
30
|
-
resolve(message);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async close() {
|
|
35
|
-
if (this._closed) return;
|
|
36
|
-
this._closed = true;
|
|
37
|
-
if (this._pendingRequest) {
|
|
38
|
-
const { reject, timeout } = this._pendingRequest;
|
|
39
|
-
clearTimeout(timeout);
|
|
40
|
-
this._pendingRequest = null;
|
|
41
|
-
reject(new Error('Transport closed'));
|
|
42
|
-
}
|
|
43
|
-
this._clearCleanupTimer();
|
|
44
|
-
this.onclose?.();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async handleRequest(message, timeoutMs = 60000) {
|
|
48
|
-
if (this._closed) {
|
|
49
|
-
throw new Error('Transport closed');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!message || !message.method) {
|
|
53
|
-
throw new Error('Invalid message');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!message.id) {
|
|
57
|
-
this.onmessage?.(message);
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
const timeout = setTimeout(() => {
|
|
63
|
-
if (this._pendingRequest?.resolve === resolve) {
|
|
64
|
-
this._pendingRequest = null;
|
|
65
|
-
reject(new Error('Request timeout'));
|
|
66
|
-
}
|
|
67
|
-
}, timeoutMs);
|
|
68
|
-
|
|
69
|
-
this._pendingRequest = { resolve, reject, timeout };
|
|
70
|
-
this.onmessage?.(message);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
resetCleanupTimer() {
|
|
75
|
-
this._clearCleanupTimer();
|
|
76
|
-
this._cleanupTimer = setTimeout(() => {
|
|
77
|
-
this.close();
|
|
78
|
-
}, SESSION_TIMEOUT);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
_clearCleanupTimer() {
|
|
82
|
-
if (this._cleanupTimer) {
|
|
83
|
-
clearTimeout(this._cleanupTimer);
|
|
84
|
-
this._cleanupTimer = null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default SessionTransport;
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { SseSession, SSE_EVENTS } from './sse-session.js';
|
|
2
|
-
|
|
3
|
-
export class SseSessionManager {
|
|
4
|
-
#sessions = new Map();
|
|
5
|
-
#eventHandlers = new Map();
|
|
6
|
-
|
|
7
|
-
constructor() {
|
|
8
|
-
this.#sessions = new Map();
|
|
9
|
-
this.#eventHandlers = new Map();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
add(clientId, session) {
|
|
13
|
-
if (!(session instanceof SseSession)) {
|
|
14
|
-
throw new Error('session must be an SseSession instance');
|
|
15
|
-
}
|
|
16
|
-
this.#sessions.set(clientId, session);
|
|
17
|
-
session.startHeartbeat();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
remove(clientId) {
|
|
21
|
-
const session = this.#sessions.get(clientId);
|
|
22
|
-
if (session) {
|
|
23
|
-
session.close();
|
|
24
|
-
this.#sessions.delete(clientId);
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get(clientId) {
|
|
31
|
-
return this.#sessions.get(clientId) || null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
has(clientId) {
|
|
35
|
-
return this.#sessions.has(clientId);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
list() {
|
|
39
|
-
return Array.from(this.#sessions.keys()).map(id => ({
|
|
40
|
-
clientId: id,
|
|
41
|
-
isActive: this.#sessions.get(id)?.isActive ?? false
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
listActive() {
|
|
46
|
-
return this.list().filter(s => s.isActive);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
count() {
|
|
50
|
-
return this.#sessions.size;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
sendToClient(clientId, event, data, id = null) {
|
|
54
|
-
const session = this.#sessions.get(clientId);
|
|
55
|
-
if (!session || !session.isActive) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return session.sendEvent(event, data, id);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
sendMessageToClient(clientId, data) {
|
|
62
|
-
return this.sendToClient(clientId, SSE_EVENTS.MESSAGE, data);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
sendErrorToClient(clientId, code, message, id = null) {
|
|
66
|
-
return this.sendToClient(clientId, SSE_EVENTS.ERROR, { code, message }, id);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
sendProgressToClient(clientId, current, total, message = '') {
|
|
70
|
-
return this.sendToClient(clientId, SSE_EVENTS.PROGRESS, { current, total, message });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
broadcast(event, data) {
|
|
74
|
-
let successCount = 0;
|
|
75
|
-
for (const [clientId, session] of this.#sessions) {
|
|
76
|
-
if (session.isActive && session.sendEvent(event, data)) {
|
|
77
|
-
successCount++;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return successCount;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
broadcastMessage(data) {
|
|
84
|
-
return this.broadcast(SSE_EVENTS.MESSAGE, data);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
broadcastError(code, message) {
|
|
88
|
-
return this.broadcast(SSE_EVENTS.ERROR, { code, message });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
on(event, handler) {
|
|
92
|
-
if (!this.#eventHandlers.has(event)) {
|
|
93
|
-
this.#eventHandlers.set(event, new Set());
|
|
94
|
-
}
|
|
95
|
-
this.#eventHandlers.get(event).add(handler);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
off(event, handler) {
|
|
99
|
-
const handlers = this.#eventHandlers.get(event);
|
|
100
|
-
if (handlers) {
|
|
101
|
-
handlers.delete(handler);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
emit(event, data) {
|
|
106
|
-
const handlers = this.#eventHandlers.get(event);
|
|
107
|
-
if (handlers) {
|
|
108
|
-
for (const handler of handlers) {
|
|
109
|
-
try {
|
|
110
|
-
handler(data);
|
|
111
|
-
} catch (e) {
|
|
112
|
-
// Ignore handler errors
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
cleanupInactive() {
|
|
119
|
-
for (const [clientId, session] of this.#sessions) {
|
|
120
|
-
if (!session.isActive) {
|
|
121
|
-
session.close();
|
|
122
|
-
this.#sessions.delete(clientId);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
closeAll() {
|
|
128
|
-
for (const [clientId, session] of this.#sessions) {
|
|
129
|
-
session.close();
|
|
130
|
-
}
|
|
131
|
-
this.#sessions.clear();
|
|
132
|
-
}
|
|
133
|
-
}
|
package/src/sse-session.js
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
4
|
-
const DEFAULT_RETRY_INTERVAL = 30000;
|
|
5
|
-
const MAX_SENT_MESSAGES = 1000;
|
|
6
|
-
|
|
7
|
-
export const SSE_EVENTS = {
|
|
8
|
-
MESSAGE: 'message',
|
|
9
|
-
ENDPOINT: 'endpoint',
|
|
10
|
-
PING: 'ping',
|
|
11
|
-
ERROR: 'error',
|
|
12
|
-
PROGRESS: 'progress',
|
|
13
|
-
CAPABILITIES: 'capabilities',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export class SseSession {
|
|
17
|
-
#res;
|
|
18
|
-
#clientId;
|
|
19
|
-
#active = true;
|
|
20
|
-
#heartbeatTimer = null;
|
|
21
|
-
#lastEventId = 0;
|
|
22
|
-
#heartbeatInterval;
|
|
23
|
-
#retryInterval;
|
|
24
|
-
#sessionData = {};
|
|
25
|
-
#sentMessages = [];
|
|
26
|
-
#backpressureBuffer = [];
|
|
27
|
-
#drainHandler = null;
|
|
28
|
-
|
|
29
|
-
constructor(res, options = {}) {
|
|
30
|
-
this.#res = res;
|
|
31
|
-
this.#clientId = options.clientId || randomUUID();
|
|
32
|
-
this.#heartbeatInterval = options.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
|
|
33
|
-
this.#retryInterval = options.retryInterval || DEFAULT_RETRY_INTERVAL;
|
|
34
|
-
this.#sessionData = options.sessionData || {};
|
|
35
|
-
|
|
36
|
-
if (options.resumeFromId && options.resumeFromId > 0) {
|
|
37
|
-
this.#lastEventId = options.resumeFromId;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this._setupSseHeaders();
|
|
41
|
-
this._sendRetry();
|
|
42
|
-
this._setupDrain();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
get clientId() {
|
|
46
|
-
return this.#clientId;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get isActive() {
|
|
50
|
-
return this.#active;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
get lastEventId() {
|
|
54
|
-
return this.#lastEventId;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
get sessionData() {
|
|
58
|
-
return this.#sessionData;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
get sentMessages() {
|
|
62
|
-
return [...this.#sentMessages];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
getMessagesSince(eventId) {
|
|
66
|
-
return this.#sentMessages.filter(m => m.id > eventId);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setSessionData(key, value) {
|
|
70
|
-
this.#sessionData[key] = value;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
getSessionData(key) {
|
|
74
|
-
return this.#sessionData[key];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
get retryInterval() {
|
|
78
|
-
return this.#retryInterval;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
setRetryInterval(ms) {
|
|
82
|
-
this.#retryInterval = ms;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
_sendRetry() {
|
|
86
|
-
this._write(`retry: ${this.#retryInterval}\n\n`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
_setupSseHeaders() {
|
|
90
|
-
this.#res.setHeader('Content-Type', 'text/event-stream');
|
|
91
|
-
this.#res.setHeader('Cache-Control', 'no-cache');
|
|
92
|
-
this.#res.setHeader('Connection', 'keep-alive');
|
|
93
|
-
this.#res.setHeader('Transfer-Encoding', 'chunked');
|
|
94
|
-
this.#res.setHeader('X-Accel-Buffering', 'no');
|
|
95
|
-
if (typeof this.#res.flush !== 'function') {
|
|
96
|
-
this.#res.flush = () => {};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
_formatEvent(event, data, id = null) {
|
|
101
|
-
const lines = [];
|
|
102
|
-
if (id !== null) {
|
|
103
|
-
lines.push(`id: ${id}`);
|
|
104
|
-
}
|
|
105
|
-
lines.push(`event: ${event}`);
|
|
106
|
-
|
|
107
|
-
if (data !== null && data !== undefined) {
|
|
108
|
-
const jsonStr = JSON.stringify(data);
|
|
109
|
-
lines.push(`data: ${jsonStr}`);
|
|
110
|
-
} else {
|
|
111
|
-
lines.push('data:');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return lines.join('\n') + '\n\n';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
_setupDrain() {
|
|
118
|
-
if (typeof this.#res.on !== 'function') return;
|
|
119
|
-
this.#drainHandler = () => {
|
|
120
|
-
if (!this.#active) return;
|
|
121
|
-
if (this.#backpressureBuffer.length > 0) {
|
|
122
|
-
this._drainBuffer();
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
this.#res.on('drain', this.#drainHandler);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
_drainBuffer() {
|
|
129
|
-
const batch = this.#backpressureBuffer.splice(0, 50);
|
|
130
|
-
for (const item of batch) {
|
|
131
|
-
const ok = this.#res.write(item);
|
|
132
|
-
if (!ok) {
|
|
133
|
-
this.#backpressureBuffer.unshift(...batch.slice(batch.indexOf(item)));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
this._flush();
|
|
138
|
-
if (this.#backpressureBuffer.length > 0) {
|
|
139
|
-
this._drainBuffer();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
_write(content) {
|
|
144
|
-
if (!this.#active) return false;
|
|
145
|
-
try {
|
|
146
|
-
if (this.#backpressureBuffer.length > 0) {
|
|
147
|
-
this.#backpressureBuffer.push(content);
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
const result = this.#res.write(content);
|
|
151
|
-
if (typeof this.#res.flush === 'function') {
|
|
152
|
-
this.#res.flush();
|
|
153
|
-
}
|
|
154
|
-
if (result === false) {
|
|
155
|
-
this.#backpressureBuffer.push(content);
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
return true;
|
|
159
|
-
} catch (e) {
|
|
160
|
-
this.#active = false;
|
|
161
|
-
this.close();
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
_flush() {
|
|
167
|
-
try {
|
|
168
|
-
if (typeof this.#res.flush === 'function') {
|
|
169
|
-
this.#res.flush();
|
|
170
|
-
}
|
|
171
|
-
} catch (e) {
|
|
172
|
-
// Ignore flush errors
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
flushBackpressure() {
|
|
177
|
-
if (this.#backpressureBuffer.length > 0) {
|
|
178
|
-
this._drainBuffer();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
sendEvent(event, data, id = null) {
|
|
183
|
-
this.#lastEventId++;
|
|
184
|
-
const eventId = id !== null ? id : this.#lastEventId;
|
|
185
|
-
const formatted = this._formatEvent(event, data, eventId);
|
|
186
|
-
this.#sentMessages.push({ event, data, id: eventId, timestamp: Date.now() });
|
|
187
|
-
if (this.#sentMessages.length > MAX_SENT_MESSAGES) {
|
|
188
|
-
this.#sentMessages.splice(0, this.#sentMessages.length - MAX_SENT_MESSAGES);
|
|
189
|
-
}
|
|
190
|
-
this._write(formatted);
|
|
191
|
-
this._flush();
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
sendMessage(data) {
|
|
196
|
-
return this.sendEvent(SSE_EVENTS.MESSAGE, data);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
sendEndpoint(path) {
|
|
200
|
-
return this.sendEvent(SSE_EVENTS.ENDPOINT, path);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
sendError(code, message, id = null) {
|
|
204
|
-
return this.sendEvent(SSE_EVENTS.ERROR, { code, message }, id);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
sendPing() {
|
|
208
|
-
return this.sendEvent(SSE_EVENTS.PING, { timestamp: Date.now() });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
sendProgress(current, total, message = '') {
|
|
212
|
-
return this.sendEvent(SSE_EVENTS.PROGRESS, { current, total, message });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
sendCapabilities(capabilities) {
|
|
216
|
-
return this.sendEvent(SSE_EVENTS.CAPABILITIES, capabilities);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
sendResponse(jsonrpcResponse, id = null) {
|
|
220
|
-
const eventId = id !== null ? id : this.#lastEventId;
|
|
221
|
-
return this.sendEvent(SSE_EVENTS.MESSAGE, jsonrpcResponse, eventId);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
startHeartbeat(interval = null) {
|
|
225
|
-
this.stopHeartbeat();
|
|
226
|
-
const ms = interval || this.#heartbeatInterval;
|
|
227
|
-
|
|
228
|
-
this.#heartbeatTimer = setInterval(() => {
|
|
229
|
-
if (!this.#active) {
|
|
230
|
-
this.stopHeartbeat();
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (!this.sendPing()) {
|
|
234
|
-
this.stopHeartbeat();
|
|
235
|
-
this.close();
|
|
236
|
-
}
|
|
237
|
-
}, ms);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
stopHeartbeat() {
|
|
241
|
-
if (this.#heartbeatTimer) {
|
|
242
|
-
clearInterval(this.#heartbeatTimer);
|
|
243
|
-
this.#heartbeatTimer = null;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
close() {
|
|
248
|
-
this.#active = false;
|
|
249
|
-
this.stopHeartbeat();
|
|
250
|
-
if (this.#drainHandler) {
|
|
251
|
-
this.#res.removeListener('drain', this.#drainHandler);
|
|
252
|
-
this.#drainHandler = null;
|
|
253
|
-
}
|
|
254
|
-
this.#backpressureBuffer = [];
|
|
255
|
-
try {
|
|
256
|
-
this.#res.end();
|
|
257
|
-
} catch (e) {
|
|
258
|
-
// Ignore errors during close
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function setupSseHeaders(res) {
|
|
264
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
265
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
266
|
-
res.setHeader('Connection', 'keep-alive');
|
|
267
|
-
res.setHeader('Transfer-Encoding', 'chunked');
|
|
268
|
-
res.setHeader('X-Accel-Buffering', 'no');
|
|
269
|
-
res.setHeader('Vary', 'Accept');
|
|
270
|
-
if (typeof res.flush !== 'function') {
|
|
271
|
-
res.flush = () => {};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let sseGlobalId = 0;
|
|
276
|
-
|
|
277
|
-
export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE, id = null) {
|
|
278
|
-
const eventId = id !== null ? id : ++sseGlobalId;
|
|
279
|
-
const lines = [
|
|
280
|
-
`id: ${eventId}`,
|
|
281
|
-
`event: ${event}`,
|
|
282
|
-
`data: ${JSON.stringify(data)}`,
|
|
283
|
-
''
|
|
284
|
-
];
|
|
285
|
-
res.write(lines.join('\n') + '\n');
|
|
286
|
-
if (typeof res.flush === 'function') res.flush();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function createSseResponseHandler(res, acceptSse = false) {
|
|
290
|
-
if (acceptSse) {
|
|
291
|
-
setupSseHeaders(res);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
send: (data, done = false) => {
|
|
296
|
-
if (acceptSse) {
|
|
297
|
-
sendSSE(res, data);
|
|
298
|
-
} else {
|
|
299
|
-
const json = JSON.stringify(data);
|
|
300
|
-
res.setHeader('Content-Type', 'application/json');
|
|
301
|
-
res.end(json);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function parseLastEventId(req) {
|
|
308
|
-
const lastEventId = req.get('Last-Event-ID') || req.get('last-event-id');
|
|
309
|
-
if (lastEventId) {
|
|
310
|
-
const parsed = parseInt(lastEventId, 10);
|
|
311
|
-
return isNaN(parsed) ? null : parsed;
|
|
312
|
-
}
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { SessionManager } from './session-manager.js';
|
|
2
|
-
|
|
3
|
-
const sessionManager = new SessionManager();
|
|
4
|
-
let _notifyFn = null;
|
|
5
|
-
|
|
6
|
-
export function setNotify(fn) {
|
|
7
|
-
_notifyFn = fn;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function subscribe(uri, sessionId) {
|
|
11
|
-
if (sessionId) {
|
|
12
|
-
return sessionManager.subscribe(sessionId, uri);
|
|
13
|
-
}
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function unsubscribe(uri, sessionId) {
|
|
18
|
-
if (sessionId) {
|
|
19
|
-
return sessionManager.unsubscribe(sessionId, uri);
|
|
20
|
-
}
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function list(sessionId) {
|
|
25
|
-
if (sessionId) {
|
|
26
|
-
return sessionManager.getSubscriptions(sessionId);
|
|
27
|
-
}
|
|
28
|
-
return Array.from(sessionManager.getAllSubscribedUris());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function isSubscribed(uri, sessionId) {
|
|
32
|
-
if (sessionId) {
|
|
33
|
-
return sessionManager.isSubscribed(sessionId, uri);
|
|
34
|
-
}
|
|
35
|
-
return sessionManager.getAllSubscribedUris().has(uri);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function notify(uri, data) {
|
|
39
|
-
if (_notifyFn) {
|
|
40
|
-
_notifyFn(uri, data);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function clear() {
|
|
45
|
-
sessionManager.clear();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function size() {
|
|
49
|
-
return sessionManager.count();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export { sessionManager };
|