# Build a Cache Key for Image Edit Inputs in Node.js

An image-editing workflow may offer a previously approved result when the same job comes back. If the request keeps the same source filename but changes the mask, reusing that result would show the wrong edit. This Node.js example builds a cache key from the actual bytes and the settings that define the job.

## 1\. Decide what makes two requests equivalent

Use the bytes your adapter will submit and the effective settings it will use. The example includes source bytes, an optional mask, the exact instruction, the model revision, the pipeline revision and output dimensions. An absent mask has its own marker. Changing the filename alone does not change the key.

The pipeline revision stands for fixed output settings in this example. If callers can vary the format, seed, strength or other settings, include each effective value in your key. Resolve defaults before calculating it.

## 2\. Build the key in a fixed field order

Save edit-key.mjs. Hash the two byte arrays individually, then serialize the fields in a fixed array. This keeps the field boundaries unambiguous and makes the result independent of the caller’s object property order.

```javascript
import { createHash } from "node:crypto";

const sha256 = (input) => createHash("sha256").update(input).digest("hex");
const hasText = (value) => typeof value === "string" && value.trim().length > 0;
const hasBytes = (value) => value instanceof Uint8Array && value.byteLength > 0;
const positive = (value) => Number.isSafeInteger(value) && value > 0;

export function imageEditKey({
  source, mask = null, instruction, modelRevision, pipelineRevision, width, height,
}) {
  if (!hasBytes(source) || (mask !== null && !hasBytes(mask))) {
    throw new Error("Provide source bytes and either mask bytes or null");
  }
  if (![instruction, modelRevision, pipelineRevision].every(hasText)) {
    throw new Error("Provide an instruction, a model revision and a pipeline revision");
  }
  if (![width, height].every(positive)) {
    throw new Error("Provide positive integer output dimensions");
  }

  const identity = [
    "image-edit/v1",
    pipelineRevision,
    modelRevision,
    instruction,
    width,
    height,
    sha256(source),
    mask === null ? null : sha256(mask),
  ];
  return sha256(JSON.stringify(identity));
}
```

## 3\. Make a local fixture

Save fixture.mjs. The byte arrays here stand in for an image and mask. They are synthetic bytes, and the model name is fictional.

```javascript
export const job = {
  source: Uint8Array.of(10, 20, 30),
  mask: Uint8Array.of(0, 255),
  instruction: "Lighten the background",
  modelRevision: "fictional-editor@7",
  pipelineRevision: "output-png@2",
  width: 800,
  height: 800,
};
```

## 4\. Try a hit and two misses

Save demo.mjs and run node demo.mjs. The map represents a single workspace’s approved outputs. The unchanged request retrieves reviewed-example.png. Changing either the mask or the model revision gives a miss.

```javascript
import { imageEditKey } from "./edit-key.mjs";
import { job } from "./fixture.mjs";

const key = imageEditKey(job);
const approved = new Map([[key, "reviewed-example.png"]]);

console.log("Same input:", approved.get(imageEditKey({ ...job })));
console.log("Changed mask:", approved.has(imageEditKey({
  ...job, mask: Uint8Array.of(255, 0),
})));
console.log("Changed model:", approved.has(imageEditKey({
  ...job, modelRevision: "fictional-editor@8",
})));
```

## 5\. Verify that important changes invalidate the key

Save test.mjs and run node test.mjs. The assertions cover the same bytes in a new array, reordered input properties, a missing mask, and changes to revisions, dimensions and instruction text.

```javascript
import assert from "node:assert/strict";
import { imageEditKey } from "./edit-key.mjs";
import { job } from "./fixture.mjs";

const key = imageEditKey(job);
const change = (fields) => imageEditKey({ ...job, ...fields });

assert.match(key, /^[a-f0-9]{64}$/);
assert.equal(key, change({ source: Uint8Array.of(10, 20, 30) }));
assert.equal(key, imageEditKey(Object.fromEntries(Object.entries(job).reverse())));
assert.notEqual(key, change({ source: Uint8Array.of(10, 20, 31) }));
assert.notEqual(key, change({ mask: Uint8Array.of(255, 0) }));
assert.notEqual(key, change({ mask: null }));
assert.notEqual(key, change({ instruction: job.instruction + " " }));
assert.notEqual(key, change({ modelRevision: "fictional-editor@8" }));
assert.notEqual(key, change({ pipelineRevision: "output-png@3" }));
assert.notEqual(key, change({ width: 801 }));
assert.notEqual(key, change({ height: 801 }));
assert.throws(() => change({ mask: new Uint8Array() }), /mask bytes/);
assert.throws(() => change({ width: 0 }), /dimensions/);
console.log("13 checks passed");
```

## 6\. Connect this to an approval step

Look up the key in the current authorized workspace. Reuse an output only if it is still accessible and eligible for reuse under your approval rules. A key identifies the inputs. It does not grant access to a saved file or guarantee what a fresh generation would produce.

On a miss, keep the request details with the new result so the reviewer can decide whether to approve it. If the key fields change later, increment the schema marker so older entries cannot be selected under the new rules.

The example and tests ran on Node.js 24.20.0 with local synthetic bytes and an in-memory map. No image was decoded or edited and no provider was contacted.

AI assistance was used to draft and edit this article.

Source workflow. [PixMind AI Image Agent Guide](https://www.pixmind.io/posts/pixmind-ai-image-agent-complete-guide)

Technical reference. [Node.js crypto Hash](https://nodejs.org/api/crypto.html#class-hash)
