@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.6 → 1.0.0-dev.7

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 CHANGED
@@ -126,6 +126,7 @@ Browser (project-b) ──┤
126
126
  - **Multiple MCP instances** can run simultaneously. Only the first instance starts the WebSocket server; others gracefully skip. All instances share the same JSONL data directory.
127
127
  - **Multi-project support** — Each project is identified by `location.origin`. Render data is stored in per-project JSONL files.
128
128
  - **No daemon required** — Each MCP instance is independent. The WebSocket server is opportunistically claimed by whichever instance starts first.
129
+ - **Value dictionary deduplication** — Render reports often repeat the same `prevValue`/`nextValue` objects across thousands of entries. Each JSONL file stores a content-addressed dictionary on its first line, mapping xxhash-wasm hashes to unique values. Render lines reference them via `@@ref:<hash>` sentinels instead of inlining the full object, dramatically reducing file size. Reads hydrate refs transparently.
129
130
 
130
131
  ## Configuration
131
132
 
@@ -2,14 +2,107 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
+ import xxhash from "xxhash-wasm";
8
9
  import { WebSocketServer } from "ws";
10
+ //#region src/server/store/utils/value-dict.ts
11
+ const DICT_KEY = "@@dict";
12
+ const REF_PREFIX = "@@ref:";
13
+ let h64ToString;
14
+ const ready = xxhash().then((api) => {
15
+ h64ToString = api.h64ToString;
16
+ });
17
+ function ensureReady() {
18
+ return ready;
19
+ }
20
+ function hashValue(value) {
21
+ return h64ToString(JSON.stringify(value));
22
+ }
23
+ function shouldDehydrate(value) {
24
+ return typeof value === "object" && value !== null;
25
+ }
26
+ function dehydrateDiffs(diffs, dict) {
27
+ if (!diffs) return false;
28
+ return diffs.map((d) => {
29
+ let { prevValue, nextValue } = d;
30
+ if (shouldDehydrate(prevValue)) {
31
+ const hash = hashValue(prevValue);
32
+ dict[hash] ??= prevValue;
33
+ prevValue = `${REF_PREFIX}${hash}`;
34
+ }
35
+ if (shouldDehydrate(nextValue)) {
36
+ const hash = hashValue(nextValue);
37
+ dict[hash] ??= nextValue;
38
+ nextValue = `${REF_PREFIX}${hash}`;
39
+ }
40
+ return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
41
+ ...d,
42
+ prevValue,
43
+ nextValue
44
+ };
45
+ });
46
+ }
47
+ function dehydrate(render, dict) {
48
+ const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
49
+ const newProps = dehydrateDiffs(propsDifferences, dict);
50
+ const newState = dehydrateDiffs(stateDifferences, dict);
51
+ const newHooks = dehydrateDiffs(hookDifferences, dict);
52
+ if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
53
+ return {
54
+ ...render,
55
+ reason: {
56
+ propsDifferences: newProps,
57
+ stateDifferences: newState,
58
+ hookDifferences: newHooks
59
+ }
60
+ };
61
+ }
62
+ function hydrateDiffs(diffs, dict) {
63
+ if (!diffs) return false;
64
+ return diffs.map((d) => {
65
+ let { prevValue, nextValue } = d;
66
+ if (typeof prevValue === "string" && prevValue.startsWith(REF_PREFIX)) prevValue = dict[prevValue.slice(6)] ?? prevValue;
67
+ if (typeof nextValue === "string" && nextValue.startsWith(REF_PREFIX)) nextValue = dict[nextValue.slice(6)] ?? nextValue;
68
+ return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
69
+ ...d,
70
+ prevValue,
71
+ nextValue
72
+ };
73
+ });
74
+ }
75
+ function hydrate(render, dict) {
76
+ const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
77
+ const newProps = hydrateDiffs(propsDifferences, dict);
78
+ const newState = hydrateDiffs(stateDifferences, dict);
79
+ const newHooks = hydrateDiffs(hookDifferences, dict);
80
+ if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
81
+ return {
82
+ ...render,
83
+ reason: {
84
+ propsDifferences: newProps,
85
+ stateDifferences: newState,
86
+ hookDifferences: newHooks
87
+ }
88
+ };
89
+ }
90
+ //#endregion
9
91
  //#region src/server/store/utils/read-jsonl.ts
10
92
  function readJsonl(file) {
11
93
  if (!existsSync(file)) return [];
12
- return readFileSync(file, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
94
+ const lines = readFileSync(file, "utf-8").split("\n").filter(Boolean);
95
+ if (lines.length === 0) return [];
96
+ let dict;
97
+ let startIdx = 0;
98
+ const first = JSON.parse(lines[0]);
99
+ if ("@@dict" in first) {
100
+ dict = first[DICT_KEY];
101
+ startIdx = 1;
102
+ }
103
+ const renders = lines.slice(startIdx).map((l) => JSON.parse(l));
104
+ if (!dict) return renders;
105
+ return renders.map((r) => hydrate(r, dict));
13
106
  }
14
107
  //#endregion
15
108
  //#region src/server/store/utils/sanitize-project-id.ts
@@ -34,6 +127,7 @@ var RenderStore = class {
34
127
  dir;
35
128
  buffers = /* @__PURE__ */ new Map();
36
129
  timers = /* @__PURE__ */ new Map();
130
+ dicts = /* @__PURE__ */ new Map();
37
131
  constructor(dir) {
38
132
  this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
39
133
  mkdirSync(this.dir, { recursive: true });
@@ -52,7 +146,14 @@ var RenderStore = class {
52
146
  buf.push(stored);
53
147
  const existing = this.timers.get(projectId);
54
148
  if (existing) clearTimeout(existing);
55
- this.timers.set(projectId, setTimeout(() => this.flush(projectId), FLUSH_DELAY_MS));
149
+ this.timers.set(projectId, setTimeout(() => {
150
+ this.flushAsync(projectId).catch((err) => console.error(`[wdyr-mcp] flush error for ${projectId}:`, err));
151
+ }, FLUSH_DELAY_MS));
152
+ }
153
+ async flushAsync(projectId) {
154
+ await ensureReady();
155
+ if (projectId) this.flushProject(projectId);
156
+ else for (const id of this.buffers.keys()) this.flushProject(id);
56
157
  }
57
158
  flush(projectId) {
58
159
  if (projectId) this.flushProject(projectId);
@@ -61,8 +162,17 @@ var RenderStore = class {
61
162
  flushProject(projectId) {
62
163
  const buf = this.buffers.get(projectId);
63
164
  if (!buf || buf.length === 0) return;
64
- const lines = buf.map((s) => JSON.stringify(s)).join("\n");
65
- appendFileSync(this.projectFile(projectId), `${lines}\n`);
165
+ let dict = this.dicts.get(projectId);
166
+ if (!dict) {
167
+ dict = {};
168
+ this.dicts.set(projectId, dict);
169
+ }
170
+ const dehydrated = buf.map((r) => dehydrate(r, dict));
171
+ const file = this.projectFile(projectId);
172
+ const existingLines = this.readDataLines(file);
173
+ const newLines = dehydrated.map((r) => JSON.stringify(r));
174
+ const allDataLines = [...existingLines, ...newLines];
175
+ writeFileSync(file, `${(Object.keys(dict).length > 0 ? [JSON.stringify({ [DICT_KEY]: dict }), ...allDataLines] : allDataLines).join("\n")}\n`);
66
176
  buf.length = 0;
67
177
  const timer = this.timers.get(projectId);
68
178
  if (timer) {
@@ -70,6 +180,14 @@ var RenderStore = class {
70
180
  this.timers.delete(projectId);
71
181
  }
72
182
  }
183
+ readDataLines(file) {
184
+ if (!existsSync(file)) return [];
185
+ return readFileSync(file, "utf-8").split("\n").filter((line) => {
186
+ if (!line) return false;
187
+ if (line.startsWith(`{"@@dict"`)) return false;
188
+ return true;
189
+ });
190
+ }
73
191
  getAllRenders(projectId) {
74
192
  this.flush(projectId);
75
193
  if (projectId) return readJsonl(this.projectFile(projectId)).map(toResult);
@@ -81,6 +199,7 @@ var RenderStore = class {
81
199
  clearRenders(projectId) {
82
200
  if (projectId) {
83
201
  this.buffers.delete(projectId);
202
+ this.dicts.delete(projectId);
84
203
  const timer = this.timers.get(projectId);
85
204
  if (timer) {
86
205
  clearTimeout(timer);
@@ -92,6 +211,7 @@ var RenderStore = class {
92
211
  for (const [id, timer] of this.timers) clearTimeout(timer);
93
212
  this.buffers.clear();
94
213
  this.timers.clear();
214
+ this.dicts.clear();
95
215
  for (const f of this.jsonlFiles()) unlinkSync(join(this.dir, f));
96
216
  }
97
217
  }
@@ -99,10 +219,14 @@ var RenderStore = class {
99
219
  this.flush();
100
220
  const projects = /* @__PURE__ */ new Set();
101
221
  for (const f of this.jsonlFiles()) {
102
- const firstLine = readFileSync(join(this.dir, f), "utf-8").split("\n")[0];
103
- if (!firstLine) continue;
104
- const stored = JSON.parse(firstLine);
105
- projects.add(stored.projectId);
222
+ const lines = readFileSync(join(this.dir, f), "utf-8").split("\n");
223
+ for (const line of lines) {
224
+ if (!line) continue;
225
+ const parsed = JSON.parse(line);
226
+ if ("@@dict" in parsed) continue;
227
+ projects.add(parsed.projectId);
228
+ break;
229
+ }
106
230
  }
107
231
  return [...projects];
108
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0x1f320.sh/why-did-you-render-mcp",
3
- "version": "1.0.0-dev.6",
3
+ "version": "1.0.0-dev.7",
4
4
  "type": "module",
5
5
  "description": "MCP server that collects why-did-you-render data from browser and exposes it to coding agents",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.12.1",
52
52
  "ws": "^8.18.0",
53
+ "xxhash-wasm": "^1.1.0",
53
54
  "zod": "^3.24.4"
54
55
  },
55
56
  "devDependencies": {