@imenam/mcp-design 1.0.13 → 1.0.15

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,10 +1,10 @@
1
1
  import { serve } from "@hono/node-server";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import { Hono } from "hono";
4
- import { streamSSE } from "hono/streaming";
5
4
  import { GUI_LISTEN_HOST, ProxyClient, createLogger, honoGuiGuard, setupLogging } from "@imenam/mcp-gui-interface";
6
5
  import path from "path";
7
6
  import fs from "fs";
7
+ import { randomUUID } from "crypto";
8
8
  import { fileURLToPath } from "url";
9
9
  import { IPCMessageSchema } from "../shared/schemas/design.schema.js";
10
10
  import { resolveLogDir } from "./core/log-dir.js";
@@ -79,7 +79,6 @@ const parentCheckInterval = setInterval(() => {
79
79
  const app = new Hono();
80
80
  app.use("*", honoGuiGuard());
81
81
  let proxyClient = null;
82
- const sseControllers = new Set();
83
82
  const pendingRequests = new Map();
84
83
  function safeSend(message) {
85
84
  if (process.send && process.connected) {
@@ -115,7 +114,23 @@ function ipcRequest(type, data, timeout = 5000) {
115
114
  }
116
115
  });
117
116
  }
118
- const PUSHED_EVENTS = new Set(["INITIAL_STATE", "DESIGNS_UPDATED", "DESIGN_UPDATED", "REMARKS_UPDATED"]);
117
+ const PUSHED_EVENTS = new Set(["DESIGNS_UPDATED", "DESIGN_UPDATED", "REMARKS_UPDATED"]);
118
+ /**
119
+ * Journal des évènements poussés par le master, relu par la GUI via
120
+ * `/api/changes`. Chaque entrée porte un numéro croissant ; seules les
121
+ * `JOURNAL_LIMIT` dernières sont gardées. `epoch` change à chaque démarrage du
122
+ * worker : un client qui le voit changer sait que sa position ne vaut plus rien.
123
+ */
124
+ const JOURNAL_LIMIT = 200;
125
+ const epoch = randomUUID();
126
+ let lastSeq = 0;
127
+ const journal = [];
128
+ function appendToJournal(event) {
129
+ lastSeq += 1;
130
+ journal.push({ seq: lastSeq, event });
131
+ if (journal.length > JOURNAL_LIMIT)
132
+ journal.splice(0, journal.length - JOURNAL_LIMIT);
133
+ }
119
134
  function setupIpcErrorHandlers() {
120
135
  process.on("error", (err) => {
121
136
  if (err.code === "ERR_IPC_CHANNEL_CLOSED")
@@ -123,7 +138,7 @@ function setupIpcErrorHandlers() {
123
138
  logger.error(`Process error: ${err.message}`);
124
139
  });
125
140
  process.on("disconnect", () => {
126
- logger.warn("IPC disconnected from MCP master. GUI worker will exit; EventSource clients will show Disconnected.");
141
+ logger.warn("IPC disconnected from MCP master. GUI worker will exit.");
127
142
  process.exit(0);
128
143
  });
129
144
  process.on("message", (msg) => {
@@ -143,16 +158,8 @@ function setupIpcErrorHandlers() {
143
158
  });
144
159
  return;
145
160
  }
146
- if (msg && PUSHED_EVENTS.has(msg.type)) {
147
- for (const controller of sseControllers) {
148
- try {
149
- controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
150
- }
151
- catch {
152
- sseControllers.delete(controller);
153
- }
154
- }
155
- }
161
+ if (msg && PUSHED_EVENTS.has(msg.type))
162
+ appendToJournal(msg);
156
163
  });
157
164
  }
158
165
  // --- Routes HTTP ------------------------------------------------------------
@@ -167,45 +174,27 @@ app.get("/proxy/health", (c) => {
167
174
  timestamp: new Date().toISOString(),
168
175
  });
169
176
  });
170
- app.get("/api/events", (c) => streamSSE(c, async (stream) => {
171
- const clientId = Math.random().toString(36).slice(2, 11);
172
- sseControllers.add(stream);
173
- logger.info(`SSE /api/events: client connected (id=${clientId})`);
174
- stream.onAbort(() => {
175
- sseControllers.delete(stream);
176
- logger.warn(`SSE /api/events: stream aborted (id=${clientId}).`);
177
- });
178
- if (!safeSend({ type: "GET_INITIAL_STATE", timestamp: new Date().toISOString() })) {
179
- logger.warn(`SSE /api/events: GET_INITIAL_STATE not sent (id=${clientId}) — IPC unavailable.`);
180
- }
181
- try {
182
- await stream.writeSSE({ data: "connected", event: "ping" });
183
- }
184
- catch (err) {
185
- logger.warn(`SSE /api/events: initial writeSSE failed (id=${clientId}): ${err?.message ?? err}`);
186
- sseControllers.delete(stream);
187
- return;
188
- }
189
- while (true) {
190
- await stream.sleep(30000);
191
- try {
192
- await stream.writeSSE({ data: "ping", event: "ping" });
193
- }
194
- catch (err) {
195
- logger.warn(`SSE /api/events: keepalive ping failed (id=${clientId}): ${err?.message ?? err}`);
196
- sseControllers.delete(stream);
197
- break;
198
- }
199
- }
200
- }));
201
- app.post("/api/client-diagnostics", async (c) => {
202
- try {
203
- logger.warn(`Client diagnostics (SSE/EventSource): ${JSON.stringify(await c.req.json())}`);
204
- }
205
- catch (e) {
206
- logger.warn(`Client diagnostics: invalid JSON body (${e?.message ?? e})`);
207
- }
208
- return c.json({ ok: true });
177
+ /**
178
+ * Évènements survenus depuis la position `since` du client. `reset: true`
179
+ * quand cette position n'est pas exploitable — absente, d'un autre démarrage du
180
+ * worker, plus ancienne que le journal ou en avance sur lui : le client
181
+ * recharge alors tout et repart de `seq`.
182
+ */
183
+ app.get("/api/changes", (c) => {
184
+ const rawSince = c.req.query("since");
185
+ const clientEpoch = c.req.query("epoch");
186
+ if (rawSince !== undefined && !/^\d+$/.test(rawSince)) {
187
+ return c.json({ error: `Invalid "since" parameter: expected a non-negative integer, got "${rawSince}"` }, 400);
188
+ }
189
+ const reset = { epoch, seq: lastSeq, reset: true };
190
+ if (rawSince === undefined || !clientEpoch || clientEpoch !== epoch)
191
+ return c.json(reset);
192
+ const since = Number(rawSince);
193
+ const oldestSeq = journal.length > 0 ? journal[0].seq : lastSeq + 1;
194
+ if (since > lastSeq || since < oldestSeq - 1)
195
+ return c.json(reset);
196
+ const events = journal.filter((entry) => entry.seq > since).map((entry) => entry.event);
197
+ return c.json({ epoch, seq: lastSeq, reset: false, events });
209
198
  });
210
199
  app.get("/api/config", async (c) => {
211
200
  const result = await ipcRequest("GET_CONFIG");
package/dist/src/index.js CHANGED
@@ -140,13 +140,6 @@ function handleIPCMessage(msg) {
140
140
  logger.info(`GUI ready at ${guiUrl}`);
141
141
  guiStartWaiter?.({ outcome: "ready", url: msg.data?.url });
142
142
  return;
143
- case "GET_INITIAL_STATE":
144
- broadcastToGUI({
145
- type: "INITIAL_STATE",
146
- data: { designs: store.list(), version: APP_VERSION },
147
- timestamp: new Date().toISOString(),
148
- });
149
- return;
150
143
  case "LIST_DESIGNS":
151
144
  reply(msg, "LIST_DESIGNS_RESPONSE", { designs: store.list() });
152
145
  return;