@atlisp/mcp 1.0.19 → 1.0.20

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.
@@ -1,5 +1,23 @@
1
1
  import { cad } from '../cad.js';
2
2
  import { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from '../constants.js';
3
+ import { log } from '../logger.js';
4
+ import config from '../config.js';
5
+
6
+ const CACHE_TTL = config.resourceCacheTtl;
7
+ const resourceCache = new Map();
8
+
9
+ function getCached(key) {
10
+ const entry = resourceCache.get(key);
11
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
12
+ return entry.data;
13
+ }
14
+ resourceCache.delete(key);
15
+ return null;
16
+ }
17
+
18
+ function setCache(key, data) {
19
+ resourceCache.set(key, { data, timestamp: Date.now() });
20
+ }
3
21
 
4
22
  function parseLispList(str) {
5
23
  if (!str || typeof str !== 'string') return null;
@@ -122,33 +140,41 @@ export async function readResource(uri) {
122
140
  }
123
141
 
124
142
  async function getCadInfo() {
143
+ const cached = getCached('cad:info');
144
+ if (cached) return cached;
145
+
125
146
  if (!cad.connected) {
126
147
  await cad.connect();
127
148
  }
128
-
149
+
129
150
  if (!cad.connected) {
130
- return {
131
- connected: false,
132
- platform: null,
133
- version: null,
134
- busy: false
135
- };
151
+ const result = { connected: false, platform: null, version: null, busy: false };
152
+ setCache('cad:info', result);
153
+ return result;
136
154
  }
137
-
155
+
138
156
  let busy = false;
139
157
  try {
140
158
  busy = await cad.isBusy();
141
- } catch (e) {}
142
-
143
- return {
159
+ } catch (e) {
160
+ log(`Error checking CAD busy status: ${e.message}`);
161
+ }
162
+
163
+ const result = {
144
164
  connected: true,
145
165
  platform: cad.getPlatform(),
146
166
  version: cad.getVersion(),
147
167
  busy
148
168
  };
169
+ setCache('cad:info', result);
170
+ return result;
149
171
  }
150
172
 
151
173
  async function getLayers(filterName = null) {
174
+ const cacheKey = filterName ? `layers:${filterName}` : 'layers:all';
175
+ const cached = getCached(cacheKey);
176
+ if (cached) return cached;
177
+
152
178
  if (!cad.connected) await cad.connect();
153
179
  if (!cad.connected) return [];
154
180
 
@@ -159,8 +185,11 @@ async function getLayers(filterName = null) {
159
185
 
160
186
  const result = await cad.sendCommandWithResult(code);
161
187
 
162
- if (!result || result === "" || result === "nil") return [];
163
-
188
+ if (!result || result === "" || result === "nil") {
189
+ setCache(cacheKey, []);
190
+ return [];
191
+ }
192
+
164
193
  let layers = [];
165
194
  try {
166
195
  layers = JSON.parse(result);
@@ -172,7 +201,7 @@ async function getLayers(filterName = null) {
172
201
  layers = [];
173
202
  }
174
203
  }
175
-
204
+
176
205
  if (!Array.isArray(layers)) {
177
206
  if (typeof layers === 'number') {
178
207
  layers = [[result, layers, 0]];
@@ -180,8 +209,8 @@ async function getLayers(filterName = null) {
180
209
  layers = [[layers, 7, 0]];
181
210
  }
182
211
  }
183
-
184
- return layers.map(l => {
212
+
213
+ const mapped = layers.map(l => {
185
214
  const name = Array.isArray(l) ? l[0] : l;
186
215
  const color = Array.isArray(l) && l[1] !== undefined ? l[1] : 7;
187
216
  const flags = Array.isArray(l) && l[2] !== undefined ? l[2] : 0;
@@ -192,58 +221,76 @@ async function getLayers(filterName = null) {
192
221
  frozen: (flags & 1) !== 0
193
222
  };
194
223
  });
224
+ setCache(cacheKey, mapped);
225
+ return mapped;
195
226
  } catch (e) {
227
+ setCache(cacheKey, []);
196
228
  return [];
197
229
  }
198
230
  }
199
231
 
200
232
  async function getEntities(filterType = null) {
233
+ const cacheKey = filterType ? `entities:${filterType}` : 'entities:all';
234
+ const cached = getCached(cacheKey);
235
+ if (cached) return cached;
236
+
201
237
  if (!cad.connected) await cad.connect();
202
- if (!cad.connected) return { total: 0, byType: {} };
203
-
238
+ if (!cad.connected) {
239
+ const result = { total: 0, byType: {} };
240
+ setCache(cacheKey, result);
241
+ return result;
242
+ }
243
+
204
244
  try {
205
245
  let code;
206
246
  if (filterType) {
207
247
  code = `(sslength (ssget "_X" (list (cons 0 "${filterType}"))))`;
208
248
  const count = await cad.sendCommandWithResult(code);
209
- return {
210
- type: filterType,
211
- count: parseInt(count) || 0
212
- };
249
+ const result = { type: filterType, count: parseInt(count) || 0 };
250
+ setCache(cacheKey, result);
251
+ return result;
213
252
  } else {
214
253
  code = `(progn(setq ss(ssget "_X"))(if ss(progn(setq elst(mapcar (quote(lambda(x)(cdr(assoc 0 (entget x))))) (pickset:to-list ss)))(stat:stat elst))))`;
215
- const result = await cad.sendCommandWithResult(code);
216
-
217
- if (!result || result === "" || result === "nil") {
218
- return { total: 0, byType: {} };
254
+ const resultStr = await cad.sendCommandWithResult(code);
255
+
256
+ if (!resultStr || resultStr === "" || resultStr === "nil") {
257
+ const result = { total: 0, byType: {} };
258
+ setCache(cacheKey, result);
259
+ return result;
219
260
  }
220
-
261
+
221
262
  let statResult = [];
222
263
  try {
223
- statResult = JSON.parse(result);
264
+ statResult = JSON.parse(resultStr);
224
265
  } catch (e) {
225
- const parsed = parseLispList(result);
266
+ const parsed = parseLispList(resultStr);
226
267
  if (parsed) {
227
268
  statResult = parsed;
228
269
  }
229
270
  }
230
-
271
+
231
272
  if (!Array.isArray(statResult)) {
232
- return { total: 0, byType: {} };
273
+ const result = { total: 0, byType: {} };
274
+ setCache(cacheKey, result);
275
+ return result;
233
276
  }
234
-
277
+
235
278
  const byType = {};
236
279
  statResult.forEach(item => {
237
280
  if (Array.isArray(item) && item.length >= 2) {
238
281
  byType[item[0]] = item[1];
239
282
  }
240
283
  });
241
-
284
+
242
285
  const total = Object.values(byType).reduce((sum, n) => sum + n, 0);
243
- return { total, byType };
286
+ const result = { total, byType };
287
+ setCache(cacheKey, result);
288
+ return result;
244
289
  }
245
290
  } catch (e) {
246
- return { total: 0, byType: {} };
291
+ const result = { total: 0, byType: {} };
292
+ setCache(cacheKey, result);
293
+ return result;
247
294
  }
248
295
  }
249
296
 
@@ -130,6 +130,4 @@ export class SseSessionManager {
130
130
  }
131
131
  this.#sessions.clear();
132
132
  }
133
- }
134
-
135
- export const globalSessionManager = new SseSessionManager();
133
+ }
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from 'crypto';
2
2
 
3
3
  const DEFAULT_HEARTBEAT_INTERVAL = 30000;
4
- const RECONNECT_DELAY = 5000;
4
+ const DEFAULT_RETRY_INTERVAL = 30000;
5
+ const MAX_SENT_MESSAGES = 1000;
5
6
 
6
7
  export const SSE_EVENTS = {
7
8
  MESSAGE: 'message',
@@ -19,15 +20,26 @@ export class SseSession {
19
20
  #heartbeatTimer = null;
20
21
  #lastEventId = 0;
21
22
  #heartbeatInterval;
23
+ #retryInterval;
22
24
  #sessionData = {};
25
+ #sentMessages = [];
26
+ #backpressureBuffer = [];
27
+ #drainHandler = null;
23
28
 
24
29
  constructor(res, options = {}) {
25
30
  this.#res = res;
26
31
  this.#clientId = options.clientId || randomUUID();
27
32
  this.#heartbeatInterval = options.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
33
+ this.#retryInterval = options.retryInterval || DEFAULT_RETRY_INTERVAL;
28
34
  this.#sessionData = options.sessionData || {};
29
35
 
36
+ if (options.resumeFromId && options.resumeFromId > 0) {
37
+ this.#lastEventId = options.resumeFromId;
38
+ }
39
+
30
40
  this._setupSseHeaders();
41
+ this._sendRetry();
42
+ this._setupDrain();
31
43
  }
32
44
 
33
45
  get clientId() {
@@ -38,10 +50,22 @@ export class SseSession {
38
50
  return this.#active;
39
51
  }
40
52
 
53
+ get lastEventId() {
54
+ return this.#lastEventId;
55
+ }
56
+
41
57
  get sessionData() {
42
58
  return this.#sessionData;
43
59
  }
44
60
 
61
+ get sentMessages() {
62
+ return [...this.#sentMessages];
63
+ }
64
+
65
+ getMessagesSince(eventId) {
66
+ return this.#sentMessages.filter(m => m.id > eventId);
67
+ }
68
+
45
69
  setSessionData(key, value) {
46
70
  this.#sessionData[key] = value;
47
71
  }
@@ -50,49 +74,122 @@ export class SseSession {
50
74
  return this.#sessionData[key];
51
75
  }
52
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
+
53
89
  _setupSseHeaders() {
54
90
  this.#res.setHeader('Content-Type', 'text/event-stream');
55
91
  this.#res.setHeader('Cache-Control', 'no-cache');
56
92
  this.#res.setHeader('Connection', 'keep-alive');
57
93
  this.#res.setHeader('Transfer-Encoding', 'chunked');
58
94
  this.#res.setHeader('X-Accel-Buffering', 'no');
95
+ if (typeof this.#res.flush !== 'function') {
96
+ this.#res.flush = () => {};
97
+ }
59
98
  }
60
99
 
61
100
  _formatEvent(event, data, id = null) {
62
- let lines = [];
101
+ const lines = [];
63
102
  if (id !== null) {
64
103
  lines.push(`id: ${id}`);
65
104
  }
66
105
  lines.push(`event: ${event}`);
67
-
68
- const jsonStr = JSON.stringify(data);
69
- lines.push(`data: ${jsonStr}`);
70
-
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
+
71
114
  return lines.join('\n') + '\n\n';
72
115
  }
73
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
+
74
143
  _write(content) {
75
144
  if (!this.#active) return false;
76
145
  try {
146
+ if (this.#backpressureBuffer.length > 0) {
147
+ this.#backpressureBuffer.push(content);
148
+ return true;
149
+ }
77
150
  const result = this.#res.write(content);
78
151
  if (typeof this.#res.flush === 'function') {
79
152
  this.#res.flush();
80
153
  }
81
154
  if (result === false) {
82
- this.#active = false;
83
- return false;
155
+ this.#backpressureBuffer.push(content);
156
+ return true;
84
157
  }
85
158
  return true;
86
159
  } catch (e) {
87
160
  this.#active = false;
161
+ this.close();
88
162
  return false;
89
163
  }
90
164
  }
91
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
+
92
182
  sendEvent(event, data, id = null) {
93
183
  this.#lastEventId++;
94
184
  const eventId = id !== null ? id : this.#lastEventId;
95
- return this._write(this._formatEvent(event, data, eventId));
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;
96
193
  }
97
194
 
98
195
  sendMessage(data) {
@@ -100,7 +197,7 @@ export class SseSession {
100
197
  }
101
198
 
102
199
  sendEndpoint(path) {
103
- return this.sendEvent(SSE_EVENTS.ENDPOINT, { path });
200
+ return this._write(`: endpoint ${path}\n\n`);
104
201
  }
105
202
 
106
203
  sendError(code, message, id = null) {
@@ -108,7 +205,7 @@ export class SseSession {
108
205
  }
109
206
 
110
207
  sendPing() {
111
- return this.sendEvent(SSE_EVENTS.PING, { timestamp: Date.now() });
208
+ return this._write(': ping ' + Date.now() + '\n\n');
112
209
  }
113
210
 
114
211
  sendProgress(current, total, message = '') {
@@ -116,7 +213,7 @@ export class SseSession {
116
213
  }
117
214
 
118
215
  sendCapabilities(capabilities) {
119
- return this.sendEvent(SSE_EVENTS.CAPABILITIES, capabilities);
216
+ return this._write(`: capabilities ${JSON.stringify(capabilities)}\n\n`);
120
217
  }
121
218
 
122
219
  sendResponse(jsonrpcResponse, id = null) {
@@ -150,6 +247,11 @@ export class SseSession {
150
247
  close() {
151
248
  this.#active = false;
152
249
  this.stopHeartbeat();
250
+ if (this.#drainHandler) {
251
+ this.#res.removeListener('drain', this.#drainHandler);
252
+ this.#drainHandler = null;
253
+ }
254
+ this.#backpressureBuffer = [];
153
255
  try {
154
256
  this.#res.end();
155
257
  } catch (e) {
@@ -163,10 +265,19 @@ export function setupSseHeaders(res) {
163
265
  res.setHeader('Cache-Control', 'no-cache');
164
266
  res.setHeader('Connection', 'keep-alive');
165
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
+ }
166
273
  }
167
274
 
168
- export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE) {
275
+ let sseGlobalId = 0;
276
+
277
+ export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE, id = null) {
278
+ const eventId = id !== null ? id : ++sseGlobalId;
169
279
  const lines = [
280
+ `id: ${eventId}`,
170
281
  `event: ${event}`,
171
282
  `data: ${JSON.stringify(data)}`,
172
283
  ''
@@ -182,11 +293,10 @@ export function createSseResponseHandler(res, acceptSse = false) {
182
293
 
183
294
  return {
184
295
  send: (data, done = false) => {
185
- const json = JSON.stringify(data);
186
296
  if (acceptSse) {
187
297
  sendSSE(res, data);
188
- if (done) res.end();
189
298
  } else {
299
+ const json = JSON.stringify(data);
190
300
  res.setHeader('Content-Type', 'application/json');
191
301
  res.end(json);
192
302
  }
@@ -1,4 +1,9 @@
1
1
  const subscriptions = new Map();
2
+ let _notifyFn = null;
3
+
4
+ export function setNotify(fn) {
5
+ _notifyFn = fn;
6
+ }
2
7
 
3
8
  export function subscribe(uri) {
4
9
  subscriptions.set(uri, Date.now());
@@ -17,6 +22,12 @@ export function isSubscribed(uri) {
17
22
  return subscriptions.has(uri);
18
23
  }
19
24
 
25
+ export function notify(uri, data) {
26
+ if (_notifyFn) {
27
+ _notifyFn(uri, data);
28
+ }
29
+ }
30
+
20
31
  export function clear() {
21
32
  subscriptions.clear();
22
33
  }