mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 21:04:04 +02:00
Collapse very long lines in response viewers (#532)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { docFingerprint } from "./docFingerprint";
|
||||
|
||||
const sampled = (text: string) => docFingerprint(text, { exact: false });
|
||||
const exact = (text: string) => docFingerprint(text, { exact: true });
|
||||
|
||||
/** Same length as `text`, with one character changed at `at` */
|
||||
const changeAt = (text: string, at: number) =>
|
||||
`${text.slice(0, at)}${text[at] === "b" ? "c" : "b"}${text.slice(at + 1)}`;
|
||||
|
||||
describe("docFingerprint", () => {
|
||||
test("is stable for the same text", () => {
|
||||
expect(sampled("hello")).toBe(sampled("hello"));
|
||||
expect(exact("hello")).toBe(exact("hello"));
|
||||
});
|
||||
|
||||
test("differs on different short text", () => {
|
||||
expect(sampled("hello")).not.toBe(sampled("world"));
|
||||
});
|
||||
|
||||
test("differs on length alone", () => {
|
||||
expect(sampled("a".repeat(1_000_000))).not.toBe(sampled("a".repeat(1_000_001)));
|
||||
});
|
||||
|
||||
test("hashes small documents in full, so any change is caught", () => {
|
||||
const doc = "a".repeat(100);
|
||||
for (let i = 0; i < doc.length; i++) {
|
||||
expect(sampled(doc)).not.toBe(sampled(changeAt(doc, i)));
|
||||
}
|
||||
});
|
||||
|
||||
describe("sampled", () => {
|
||||
const doc = "a".repeat(1_000_000);
|
||||
|
||||
test("notices a change at the start", () => {
|
||||
expect(sampled(doc)).not.toBe(sampled(changeAt(doc, 0)));
|
||||
});
|
||||
|
||||
test("notices a change at the end", () => {
|
||||
expect(sampled(doc)).not.toBe(sampled(changeAt(doc, doc.length - 1)));
|
||||
});
|
||||
|
||||
test("notices a change in the middle", () => {
|
||||
expect(sampled(doc)).not.toBe(sampled(changeAt(doc, doc.length / 2)));
|
||||
});
|
||||
|
||||
test("misses a same-length change between the samples", () => {
|
||||
// The documented cost of sampling. Read-only editors accept it because the worst a
|
||||
// collision can do there is restore a fold or the cursor to the wrong place.
|
||||
expect(sampled(doc)).toBe(sampled(changeAt(doc, 250_000)));
|
||||
});
|
||||
});
|
||||
|
||||
describe("exact", () => {
|
||||
const doc = "a".repeat(1_000_000);
|
||||
|
||||
test("catches a same-length change anywhere, which is why editable docs use it", () => {
|
||||
for (const at of [0, 250_000, doc.length / 2, 750_000, doc.length - 1]) {
|
||||
expect(exact(doc)).not.toBe(exact(changeAt(doc, at)));
|
||||
}
|
||||
});
|
||||
|
||||
test("does not collide with the sampled fingerprint of the same text", () => {
|
||||
expect(exact(doc)).not.toBe(sampled(doc));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { md5 } from "js-md5";
|
||||
|
||||
/** How much of each end and the middle to hash */
|
||||
const SAMPLE_CHARS = 512;
|
||||
|
||||
/**
|
||||
* Identifies a document, so a cached editor state is never restored onto different content.
|
||||
*
|
||||
* Hashing the whole document costs about 4 ms per megabyte and is paid on every editor update
|
||||
* as well as on restore, which is a lot of work for a document nobody can edit.
|
||||
*
|
||||
* `exact` decides how much certainty that buys. Editable documents get a full hash, because a
|
||||
* collision there would restore undo history belonging to other content and let an undo write
|
||||
* nonsense into the body. Read-only documents get a sampled one: they hold the megabyte-sized
|
||||
* responses this exists for, their history can never be applied, and the worst a collision can
|
||||
* do is put a fold or the cursor in the wrong place.
|
||||
*
|
||||
* Sampling still pins the exact length plus three windows, so a colliding pair has to agree on
|
||||
* all four and differ only in between. Documents small enough to hash outright still are.
|
||||
*/
|
||||
export function docFingerprint(text: string, { exact }: { exact: boolean }): string {
|
||||
if (exact || text.length <= SAMPLE_CHARS * 3) {
|
||||
return `${text.length}:${md5(text)}`;
|
||||
}
|
||||
|
||||
const middle = Math.floor((text.length - SAMPLE_CHARS) / 2);
|
||||
return [
|
||||
text.length,
|
||||
md5(text.slice(0, SAMPLE_CHARS)),
|
||||
md5(text.slice(middle, middle + SAMPLE_CHARS)),
|
||||
md5(text.slice(-SAMPLE_CHARS)),
|
||||
].join(":");
|
||||
}
|
||||
Reference in New Issue
Block a user