@engine-room/after-effects-mcp 0.5.1 → 0.6.0

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.
Files changed (37) hide show
  1. package/README.md +126 -185
  2. package/bin/server.js +12567 -2478
  3. package/package.json +8 -8
  4. package/panel/CSXS/manifest.xml +44 -4
  5. package/panel/META-INF/signatures.xml +182 -0
  6. package/panel/client/csinterface.js +32 -0
  7. package/panel/client/main.js +522 -632
  8. package/panel/client/panelhash.js +79 -0
  9. package/panel/client/status.html +27 -0
  10. package/panel/client/status.js +112 -0
  11. package/panel/jsx/bundle.jsx +9032 -2393
  12. package/panel/mimetype +1 -0
  13. package/panel/node_modules/ws/LICENSE +20 -0
  14. package/panel/node_modules/ws/README.md +548 -0
  15. package/panel/node_modules/ws/browser.js +8 -0
  16. package/panel/node_modules/ws/index.js +22 -0
  17. package/panel/node_modules/ws/lib/buffer-util.js +131 -0
  18. package/panel/node_modules/ws/lib/constants.js +19 -0
  19. package/panel/node_modules/ws/lib/event-target.js +292 -0
  20. package/panel/node_modules/ws/lib/extension.js +203 -0
  21. package/panel/node_modules/ws/lib/limiter.js +55 -0
  22. package/panel/node_modules/ws/lib/permessage-deflate.js +530 -0
  23. package/panel/node_modules/ws/lib/receiver.js +743 -0
  24. package/panel/node_modules/ws/lib/sender.js +607 -0
  25. package/panel/node_modules/ws/lib/stream.js +161 -0
  26. package/panel/node_modules/ws/lib/subprotocol.js +62 -0
  27. package/panel/node_modules/ws/lib/validation.js +152 -0
  28. package/panel/node_modules/ws/lib/websocket-server.js +562 -0
  29. package/panel/node_modules/ws/lib/websocket.js +1421 -0
  30. package/panel/node_modules/ws/package.json +74 -0
  31. package/panel/node_modules/ws/wrapper.mjs +21 -0
  32. package/panel/package.json +2 -2
  33. package/panel/client/contactsheet.js +0 -347
  34. package/panel/client/framecache.js +0 -98
  35. package/panel/client/framereader.js +0 -248
  36. package/panel/client/mogrt.js +0 -328
  37. package/panel/client/pngcodec.js +0 -524
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "ws",
3
+ "version": "8.22.0",
4
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
+ "keywords": [
6
+ "HyBi",
7
+ "Push",
8
+ "RFC-6455",
9
+ "WebSocket",
10
+ "WebSockets",
11
+ "real-time"
12
+ ],
13
+ "homepage": "https://github.com/websockets/ws",
14
+ "bugs": "https://github.com/websockets/ws/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/websockets/ws.git"
18
+ },
19
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
20
+ "license": "MIT",
21
+ "main": "index.js",
22
+ "exports": {
23
+ ".": {
24
+ "browser": "./browser.js",
25
+ "import": "./wrapper.mjs",
26
+ "require": "./index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "browser": "browser.js",
31
+ "engines": {
32
+ "node": ">=10.0.0"
33
+ },
34
+ "files": [
35
+ "browser.js",
36
+ "index.js",
37
+ "lib/*.js",
38
+ "wrapper.mjs"
39
+ ],
40
+ "scripts": {
41
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
42
+ "integration": "mocha --throw-deprecation test/*.integration.js",
43
+ "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
44
+ },
45
+ "peerDependencies": {
46
+ "bufferutil": "^4.0.1",
47
+ "utf-8-validate": ">=5.0.2"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "bufferutil": {
51
+ "optional": true
52
+ },
53
+ "utf-8-validate": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
59
+ "benchmark": "^2.1.4",
60
+ "bufferutil": "^4.0.1",
61
+ "eslint": "^10.0.1",
62
+ "eslint-config-prettier": "^10.0.1",
63
+ "eslint-plugin-prettier": "^5.0.0",
64
+ "globals": "^17.0.0",
65
+ "mocha": "^8.4.0",
66
+ "nyc": "^15.0.0",
67
+ "prettier": "^3.0.0",
68
+ "utf-8-validate": "^6.0.0"
69
+ },
70
+ "allowScripts": {
71
+ "bufferutil": true,
72
+ "utf-8-validate": true
73
+ }
74
+ }
@@ -0,0 +1,21 @@
1
+ import createWebSocketStream from './lib/stream.js';
2
+ import extension from './lib/extension.js';
3
+ import PerMessageDeflate from './lib/permessage-deflate.js';
4
+ import Receiver from './lib/receiver.js';
5
+ import Sender from './lib/sender.js';
6
+ import subprotocol from './lib/subprotocol.js';
7
+ import WebSocket from './lib/websocket.js';
8
+ import WebSocketServer from './lib/websocket-server.js';
9
+
10
+ export {
11
+ createWebSocketStream,
12
+ extension,
13
+ PerMessageDeflate,
14
+ Receiver,
15
+ Sender,
16
+ subprotocol,
17
+ WebSocket,
18
+ WebSocketServer
19
+ };
20
+
21
+ export default WebSocket;
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@engineroom/ae-panel",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "description": "Invisible CEP extension hosting an HTTP+WS bridge inside After Effects.",
6
6
  "dependencies": {
7
- "ws": "^8.21.3"
7
+ "ws": "^8.22.0"
8
8
  }
9
9
  }
@@ -1,347 +0,0 @@
1
- // contactsheet.js — tile several rendered frames into one labelled sheet.
2
- //
3
- // Judging motion is one visual question — "does it move the way I think it
4
- // does" — and answering it used to cost three `screenshot_frame` calls. Three
5
- // calls is three image blocks resident in the context for the rest of the
6
- // session, three chances for After Effects to re-serve a stale buffer, and no
7
- // guarantee the agent lines the frames up in the right order. One sheet is one
8
- // image, one op, and the order is in the picture. Issue #56.
9
- //
10
- // The compositing happens here rather than in ExtendScript for the obvious
11
- // reason: `packages/jsx` has no pixels, no PNG encoder and no way to draw a
12
- // character. The panel already decodes and re-encodes frames for the 16-bit
13
- // conversion, so the sheet is a few hundred lines on top of machinery that
14
- // exists.
15
- //
16
- // Two rules the layout has to keep:
17
- //
18
- // * Every requested time gets a cell, in order, whether or not it rendered.
19
- // A sheet that silently drops the tile that failed no longer maps onto the
20
- // times that were asked for, and an agent counting tiles left to right
21
- // would read the wrong frame as the right one.
22
- // * The time is burned into the picture. Metadata beside an image is not
23
- // what a model looks at when it is comparing three frames.
24
- //
25
- // Node builtins only, and a CommonJS module, for the same reasons as
26
- // pngcodec.js: `client/main.js` requires it inside CEP's mixed context and
27
- // `tests/unit/contact-sheet.mjs` requires the same file under plain Node.
28
-
29
- "use strict";
30
-
31
- var pngCodec = require("./pngcodec.js");
32
-
33
- // ---------------------------------------------------------------------------
34
- // Layout
35
- // ---------------------------------------------------------------------------
36
-
37
- // Columns per tile count. Not a formula, because the two things a formula gets
38
- // wrong are the ones that matter: three frames of an animation read as a strip
39
- // and should stay on one row, and five should not leave a hole in the middle of
40
- // a 2-wide grid. Two to six is the whole domain — the schema caps `times` there
41
- // — so the table is complete rather than a heuristic.
42
- var COLUMNS_FOR = { 2: 2, 3: 3, 4: 2, 5: 3, 6: 3 };
43
-
44
- function layoutFor(count) {
45
- var cols = COLUMNS_FOR[count];
46
- if (!cols) cols = Math.ceil(Math.sqrt(count));
47
- return { cols: cols, rows: Math.ceil(count / cols) };
48
- }
49
-
50
- // The gutter is opaque on purpose. A transparent one would be invisible behind
51
- // a transparent overlay comp — which is exactly the kind of comp somebody
52
- // screenshots three times — and the tiles would run together.
53
- var GUTTER = 4;
54
- var BACKGROUND = [38, 38, 38, 255];
55
- var TILE_BORDER = [150, 150, 150, 255];
56
-
57
- // Flat blocks for the cells that have no picture. Each carries its status word
58
- // in the label chip as well; the colour alone is a hint, never the statement.
59
- var STATUS_FILL = {
60
- empty: [22, 22, 28, 255],
61
- stale: [74, 30, 30, 255],
62
- failed: [74, 50, 20, 255],
63
- };
64
-
65
- // ---------------------------------------------------------------------------
66
- // A 5x7 bitmap font, five column bytes per glyph, bit 0 = top row.
67
- //
68
- // Burning text into a frame needs a font, and a font file would be a second
69
- // thing to ship into the CEP extension and keep in sync — the problem `ws`
70
- // already causes once. Fifty glyphs of pixels weigh nothing and render
71
- // identically on both platforms, which a system font would not.
72
- // ---------------------------------------------------------------------------
73
- var FONT = {
74
- "0": [0x3e, 0x51, 0x49, 0x45, 0x3e],
75
- "1": [0x00, 0x42, 0x7f, 0x40, 0x00],
76
- "2": [0x42, 0x61, 0x51, 0x49, 0x46],
77
- "3": [0x21, 0x41, 0x45, 0x4b, 0x31],
78
- "4": [0x18, 0x14, 0x12, 0x7f, 0x10],
79
- "5": [0x27, 0x45, 0x45, 0x45, 0x39],
80
- "6": [0x3c, 0x4a, 0x49, 0x49, 0x30],
81
- "7": [0x01, 0x71, 0x09, 0x05, 0x03],
82
- "8": [0x36, 0x49, 0x49, 0x49, 0x36],
83
- "9": [0x06, 0x49, 0x49, 0x29, 0x1e],
84
- ".": [0x00, 0x60, 0x60, 0x00, 0x00],
85
- "-": [0x08, 0x08, 0x08, 0x08, 0x08],
86
- ":": [0x00, 0x36, 0x36, 0x00, 0x00],
87
- " ": [0x00, 0x00, 0x00, 0x00, 0x00],
88
- s: [0x48, 0x54, 0x54, 0x54, 0x20],
89
- A: [0x7e, 0x11, 0x11, 0x11, 0x7e],
90
- B: [0x7f, 0x49, 0x49, 0x49, 0x36],
91
- C: [0x3e, 0x41, 0x41, 0x41, 0x22],
92
- D: [0x7f, 0x41, 0x41, 0x22, 0x1c],
93
- E: [0x7f, 0x49, 0x49, 0x49, 0x41],
94
- F: [0x7f, 0x09, 0x09, 0x09, 0x01],
95
- G: [0x3e, 0x41, 0x49, 0x49, 0x7a],
96
- H: [0x7f, 0x08, 0x08, 0x08, 0x7f],
97
- I: [0x00, 0x41, 0x7f, 0x41, 0x00],
98
- J: [0x20, 0x40, 0x41, 0x3f, 0x01],
99
- K: [0x7f, 0x08, 0x14, 0x22, 0x41],
100
- L: [0x7f, 0x40, 0x40, 0x40, 0x40],
101
- M: [0x7f, 0x02, 0x0c, 0x02, 0x7f],
102
- N: [0x7f, 0x04, 0x08, 0x10, 0x7f],
103
- O: [0x3e, 0x41, 0x41, 0x41, 0x3e],
104
- P: [0x7f, 0x09, 0x09, 0x09, 0x06],
105
- Q: [0x3e, 0x41, 0x51, 0x21, 0x5e],
106
- R: [0x7f, 0x09, 0x19, 0x29, 0x46],
107
- S: [0x46, 0x49, 0x49, 0x49, 0x31],
108
- T: [0x01, 0x01, 0x7f, 0x01, 0x01],
109
- U: [0x3f, 0x40, 0x40, 0x40, 0x3f],
110
- V: [0x1f, 0x20, 0x40, 0x20, 0x1f],
111
- W: [0x3f, 0x40, 0x38, 0x40, 0x3f],
112
- X: [0x63, 0x14, 0x08, 0x14, 0x63],
113
- Y: [0x07, 0x08, 0x70, 0x08, 0x07],
114
- Z: [0x61, 0x51, 0x49, 0x45, 0x43],
115
- };
116
- var GLYPH_W = 5;
117
- var GLYPH_H = 7;
118
- var ADVANCE = GLYPH_W + 1;
119
-
120
- function glyphFor(ch) {
121
- if (FONT[ch]) return FONT[ch];
122
- var up = ch.toUpperCase();
123
- if (FONT[up]) return FONT[up];
124
- return FONT[" "];
125
- }
126
-
127
- /** Width in pixels of `text` at `scale`, with no trailing inter-glyph gap. */
128
- function textWidth(text, scale) {
129
- if (!text.length) return 0;
130
- return (text.length * ADVANCE - 1) * scale;
131
- }
132
-
133
- /**
134
- * Format a time for the label: shortest form that is still unambiguous.
135
- * 0 -> "0s", 1.5 -> "1.5s", 2.25 -> "2.25s", 0.041666 -> "0.042s".
136
- *
137
- * Three decimals because a 30fps frame is 0.033s apart from its neighbour and
138
- * two would print two adjacent frames identically — the one case where the
139
- * label would actively mislead.
140
- */
141
- function formatTime(t) {
142
- if (typeof t !== "number" || !isFinite(t)) return "?s";
143
- var s = t.toFixed(3);
144
- // Trim the zeros the fixed form adds, but never the digit before the point.
145
- s = s.replace(/0+$/, "").replace(/\.$/, "");
146
- return s + "s";
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
- // Drawing
151
- // ---------------------------------------------------------------------------
152
-
153
- function setPixel(sheet, sheetW, sheetH, x, y, rgba) {
154
- if (x < 0 || y < 0 || x >= sheetW || y >= sheetH) return;
155
- var o = (y * sheetW + x) * 4;
156
- sheet[o] = rgba[0];
157
- sheet[o + 1] = rgba[1];
158
- sheet[o + 2] = rgba[2];
159
- sheet[o + 3] = rgba[3];
160
- }
161
-
162
- function fillRect(sheet, sheetW, sheetH, x, y, w, h, rgba) {
163
- for (var j = 0; j < h; j++) {
164
- for (var i = 0; i < w; i++) setPixel(sheet, sheetW, sheetH, x + i, y + j, rgba);
165
- }
166
- }
167
-
168
- function strokeRect(sheet, sheetW, sheetH, x, y, w, h, rgba) {
169
- for (var i = 0; i < w; i++) {
170
- setPixel(sheet, sheetW, sheetH, x + i, y, rgba);
171
- setPixel(sheet, sheetW, sheetH, x + i, y + h - 1, rgba);
172
- }
173
- for (var j = 0; j < h; j++) {
174
- setPixel(sheet, sheetW, sheetH, x, y + j, rgba);
175
- setPixel(sheet, sheetW, sheetH, x + w - 1, y + j, rgba);
176
- }
177
- }
178
-
179
- /**
180
- * Draw `text` in an opaque chip whose top-left corner is (x, y).
181
- *
182
- * Opaque rather than blended: the chip sits on top of whatever the frame
183
- * happens to be, and a translucent one over a busy frame is the one case where
184
- * the label becomes unreadable — which defeats the whole point of burning it in.
185
- *
186
- * Returns the chip's { x, y, width, height } so a caller can assert where it
187
- * landed.
188
- */
189
- function drawLabel(sheet, sheetW, sheetH, x, y, text, scale, ink, chip) {
190
- var pad = 2 * scale;
191
- var w = textWidth(text, scale) + pad * 2;
192
- var h = GLYPH_H * scale + pad * 2;
193
- fillRect(sheet, sheetW, sheetH, x, y, w, h, chip);
194
- var penX = x + pad;
195
- for (var c = 0; c < text.length; c++) {
196
- var glyph = glyphFor(text.charAt(c));
197
- for (var col = 0; col < GLYPH_W; col++) {
198
- var bits = glyph[col];
199
- for (var row = 0; row < GLYPH_H; row++) {
200
- if (!(bits & (1 << row))) continue;
201
- fillRect(
202
- sheet, sheetW, sheetH,
203
- penX + col * scale, y + pad + row * scale,
204
- scale, scale, ink
205
- );
206
- }
207
- }
208
- penX += ADVANCE * scale;
209
- }
210
- return { x: x, y: y, width: w, height: h };
211
- }
212
-
213
- /**
214
- * Copy one decoded frame into a cell, converting whatever channel count it
215
- * arrived with to the sheet's RGBA.
216
- *
217
- * Frames larger than the cell are cropped rather than resampled. They should
218
- * never be — every tile in a sheet is rendered at the same downsample from the
219
- * same comp — but a crop keeps a surprise visible in the picture, where a
220
- * silent resample would make a wrong frame look like a right one.
221
- */
222
- function blitTile(sheet, sheetW, sheetH, dstX, dstY, cellW, cellH, tile) {
223
- var ch = tile.channels;
224
- var w = Math.min(tile.width, cellW);
225
- var h = Math.min(tile.height, cellH);
226
- for (var y = 0; y < h; y++) {
227
- for (var x = 0; x < w; x++) {
228
- var si = (y * tile.width + x) * ch;
229
- var r, g, b, a;
230
- if (ch === 1) { r = g = b = tile.pixels[si]; a = 255; }
231
- else if (ch === 2) { r = g = b = tile.pixels[si]; a = tile.pixels[si + 1]; }
232
- else if (ch === 3) { r = tile.pixels[si]; g = tile.pixels[si + 1]; b = tile.pixels[si + 2]; a = 255; }
233
- else { r = tile.pixels[si]; g = tile.pixels[si + 1]; b = tile.pixels[si + 2]; a = tile.pixels[si + 3]; }
234
- var o = ((dstY + y) * sheetW + (dstX + x)) * 4;
235
- sheet[o] = r;
236
- sheet[o + 1] = g;
237
- sheet[o + 2] = b;
238
- sheet[o + 3] = a;
239
- }
240
- }
241
- }
242
-
243
- /**
244
- * Compose the sheet.
245
- *
246
- * `tiles` is one entry per *requested* time, in order:
247
- * { time, status, pixels?, width?, height?, channels?, note? }
248
- * where status is "ok" for a frame that rendered and anything in STATUS_FILL
249
- * ("empty", "stale", "failed") for one that did not.
250
- *
251
- * `opts.cellWidth`/`cellHeight` are what a tile is expected to measure — passed
252
- * in rather than taken from the first tile, because the first tile is exactly
253
- * the one that may have failed and have no dimensions at all.
254
- *
255
- * Returns { buffer, width, height, cols, rows, cellWidth, cellHeight, tiles },
256
- * where each entry of `tiles` carries the cell rectangle it was drawn into.
257
- */
258
- function composeContactSheet(tiles, opts) {
259
- if (!tiles || !tiles.length) throw new Error("composeContactSheet needs at least one tile");
260
- var o = opts || {};
261
- var cellW = o.cellWidth;
262
- var cellH = o.cellHeight;
263
- for (var t = 0; t < tiles.length; t++) {
264
- if (tiles[t].status === "ok" && tiles[t].width && tiles[t].height) {
265
- // A rendered frame is the authority on its own size; the expectation is
266
- // only a fallback for the cells that have no picture.
267
- if (tiles[t].width > cellW) cellW = tiles[t].width;
268
- if (tiles[t].height > cellH) cellH = tiles[t].height;
269
- }
270
- }
271
- if (!(cellW > 0) || !(cellH > 0)) throw new Error("composeContactSheet needs a positive cell size");
272
-
273
- var lay = layoutFor(tiles.length);
274
- var cols = lay.cols;
275
- var rows = lay.rows;
276
- var gutter = o.gutter === undefined ? GUTTER : o.gutter;
277
- var sheetW = cols * cellW + (cols + 1) * gutter;
278
- var sheetH = rows * cellH + (rows + 1) * gutter;
279
-
280
- var sheet = Buffer.alloc(sheetW * sheetH * 4);
281
- fillRect(sheet, sheetW, sheetH, 0, 0, sheetW, sheetH, BACKGROUND);
282
-
283
- // One label size for the whole sheet, from the cell width, so every tile is
284
- // annotated identically and a 4K sheet does not get a 6px caption.
285
- var scale = o.labelScale;
286
- if (!scale) scale = Math.max(1, Math.min(4, Math.round(cellW / 300)));
287
-
288
- var out = [];
289
- for (var i = 0; i < tiles.length; i++) {
290
- var tile = tiles[i];
291
- var col = i % cols;
292
- var row = Math.floor(i / cols);
293
- var x = gutter + col * (cellW + gutter);
294
- var y = gutter + row * (cellH + gutter);
295
-
296
- if (tile.status === "ok" && tile.pixels) {
297
- blitTile(sheet, sheetW, sheetH, x, y, cellW, cellH, tile);
298
- } else {
299
- var fill = STATUS_FILL[tile.status] || STATUS_FILL.failed;
300
- fillRect(sheet, sheetW, sheetH, x, y, cellW, cellH, fill);
301
- }
302
- strokeRect(sheet, sheetW, sheetH, x, y, cellW, cellH, TILE_BORDER);
303
-
304
- var text = formatTime(tile.time);
305
- if (tile.status !== "ok") text += " " + String(tile.status).toUpperCase();
306
- var chip = drawLabel(
307
- sheet, sheetW, sheetH,
308
- x + gutter, y + gutter,
309
- text, scale,
310
- [255, 255, 255, 255], [0, 0, 0, 255]
311
- );
312
-
313
- out.push({
314
- time: tile.time,
315
- status: tile.status,
316
- x: x,
317
- y: y,
318
- width: cellW,
319
- height: cellH,
320
- label: text,
321
- labelRect: chip,
322
- note: tile.note,
323
- });
324
- }
325
-
326
- return {
327
- buffer: pngCodec.encodePng8(sheetW, sheetH, 6, sheet),
328
- width: sheetW,
329
- height: sheetH,
330
- cols: cols,
331
- rows: rows,
332
- cellWidth: cellW,
333
- cellHeight: cellH,
334
- tiles: out,
335
- };
336
- }
337
-
338
- module.exports = {
339
- composeContactSheet: composeContactSheet,
340
- // Exported for the tests, which have to be able to state the expected sheet
341
- // size without re-deriving the layout from the code under test.
342
- layoutFor: layoutFor,
343
- formatTime: formatTime,
344
- textWidth: textWidth,
345
- GLYPH_H: GLYPH_H,
346
- GUTTER: GUTTER,
347
- };
@@ -1,98 +0,0 @@
1
- // framecache.js — recent screenshot identity, for spotting a stale render.
2
- //
3
- // After Effects' render path sometimes answers a screenshot request with a
4
- // buffer it produced for an earlier one, and reports success either way. Nothing
5
- // else in the response distinguishes the two: the temp file is freshly named,
6
- // the op returns ok, the PNG is well formed. The one signal that survives is
7
- // content identity — two *different* requests coming back with byte-identical
8
- // pixels is not a coincidence, it is one buffer handed out twice. Reported and
9
- // evidenced as issue #29: byte-identical results for unrelated comps at
10
- // unrelated times, and at different downsample factors, which cannot even be the
11
- // same number of pixels.
12
- //
13
- // This lives in the panel rather than in the MCP server for two reasons. The
14
- // panel is the only thing that sees every render — the documented workaround for
15
- // this bug POSTs /op directly, and the check has to hold for that caller too.
16
- // And it outlives any one MCP client process, which is the right lifetime for
17
- // "what has this After Effects session rendered recently".
18
- //
19
- // Kept in its own file so tests/unit/frame-cache.mjs can require it without a
20
- // DOM or a running After Effects.
21
-
22
- "use strict";
23
-
24
- // Enough history to catch the bug — it shows up within a handful of calls — and
25
- // small enough that a long session cannot turn a linear scan into a cost.
26
- var DEFAULT_LIMIT = 24;
27
- // Long enough to span a working sequence of screenshots, short enough that a
28
- // comp genuinely edited between two visits is not compared against its own past.
29
- var DEFAULT_TTL_MS = 10 * 60 * 1000;
30
-
31
- function createFrameCache(options) {
32
- var opts = options || {};
33
- var limit = opts.limit || DEFAULT_LIMIT;
34
- var ttlMs = opts.ttlMs || DEFAULT_TTL_MS;
35
- var now = opts.now || function () { return Date.now(); };
36
- var entries = [];
37
-
38
- function prune() {
39
- var cutoff = now() - ttlMs;
40
- var kept = [];
41
- for (var i = 0; i < entries.length; i++) {
42
- if (entries[i].at > cutoff) kept.push(entries[i]);
43
- }
44
- entries = kept;
45
- }
46
-
47
- return {
48
- /**
49
- * The most recent *different* request that produced these exact pixels, or
50
- * null. Matching the same key proves nothing: repeating a screenshot is
51
- * allowed to return the same picture, and an agent retrying after a stale
52
- * report must not be told the retry is stale too.
53
- */
54
- match: function (key, hash) {
55
- prune();
56
- for (var i = entries.length - 1; i >= 0; i--) {
57
- if (entries[i].hash === hash && entries[i].key !== key) {
58
- return {
59
- key: entries[i].key,
60
- label: entries[i].label,
61
- bytes: entries[i].bytes,
62
- ageMs: now() - entries[i].at,
63
- };
64
- }
65
- }
66
- return null;
67
- },
68
-
69
- /**
70
- * Record a frame that was actually delivered. Only successful, non-empty
71
- * renders belong here: an empty frame is legitimately identical to every
72
- * other empty frame, and remembering rejected ones would make the first
73
- * stale buffer poison every later request that happens to repeat it.
74
- */
75
- remember: function (key, hash, meta) {
76
- prune();
77
- // One entry per request key, so re-running the same screenshot replaces
78
- // its own record instead of filling the window with copies of itself.
79
- var kept = [];
80
- for (var i = 0; i < entries.length; i++) {
81
- if (entries[i].key !== key) kept.push(entries[i]);
82
- }
83
- entries = kept;
84
- entries.push({
85
- key: key,
86
- hash: hash,
87
- at: now(),
88
- label: (meta && meta.label) || key,
89
- bytes: (meta && meta.bytes) || 0,
90
- });
91
- while (entries.length > limit) entries.shift();
92
- },
93
-
94
- size: function () { prune(); return entries.length; },
95
- };
96
- }
97
-
98
- module.exports = { createFrameCache: createFrameCache };