abort in-flight kitty chunks on delete

This commit is contained in:
Anthony Kim
2026-02-09 13:34:27 -08:00
parent d7ba56cd28
commit c11a8f43f4
2 changed files with 83 additions and 1 deletions
@@ -431,6 +431,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
const id = cmd.id;
if (id !== undefined) {
// Abort in-flight chunked upload for this specific image
const pending = this._pendingTransmissions.get(id);
if (pending) {
pending.decoder.release();
this._pendingTransmissions.delete(id);
}
this._images.delete(id);
const storageId = this._kittyIdToStorageId.get(id);
if (storageId !== undefined) {
@@ -438,6 +445,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
this._kittyIdToStorageId.delete(id);
}
} else {
// Abort all in-flight chunked uploads
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._images.clear();
for (const storageId of this._kittyIdToStorageId.values()) {
this._storage.deleteImage(storageId);
@@ -607,4 +620,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
public get images(): ReadonlyMap<number, IKittyImageData> {
return this._images;
}
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
return this._pendingTransmissions;
}
}
+66 -1
View File
@@ -6,7 +6,7 @@
import test from '@playwright/test';
import { readFileSync } from 'fs';
import { ITestContext, createTestContext, openTerminal, timeout } from '../../../test/playwright/TestUtils';
import { deepStrictEqual, strictEqual } from 'assert';
import { deepStrictEqual, ok, strictEqual } from 'assert';
/**
* Plugin ctor options.
@@ -245,6 +245,71 @@ test.describe('Kitty Graphics Protocol', () => {
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
});
test('delete by id aborts in-flight chunked upload', async () => {
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=50,m=1;${part1}\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 1);
await ctx.proxy.write(`\x1b_Ga=d,i=50\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 0);
});
test('delete by id only aborts targeted upload, not others', async () => {
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=55,m=1;${part1}\x1b\\`);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=56,m=1;${part1}\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 2);
await ctx.proxy.write(`\x1b_Ga=d,i=55\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 1);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.has(56)`), true);
});
test('delete all aborts in-flight chunked upload', async () => {
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=60,m=1;${part1}\x1b\\`);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=61,m=1;${part1}\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 2);
await ctx.proxy.write(`\x1b_Ga=d\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 0);
});
test('chunks sent after delete are not assembled with previous data', async () => {
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=70,m=1;${part1}\x1b\\`);
await timeout(50);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 1);
await ctx.proxy.write(`\x1b_Ga=d\x1b\\`);
await timeout(50);
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=70;${part2}\x1b\\`);
await timeout(100);
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(70)`), true);
const storedSize: number = await ctx.page.evaluate(async () => {
const blob = (window as any).imageAddon._handlers.get('kitty').images.get(70).data;
return blob.size;
});
ok(storedSize < KITTY_BLACK_1X1_BYTES.length, 'stored data should be smaller than full image (only second half)');
});
});
test.describe('Query support (a=q)', () => {