fix more mem leak + cleanups

This commit is contained in:
Anthony Kim
2026-02-15 20:00:16 -08:00
parent 5efff43e93
commit d5ae437dda
2 changed files with 36 additions and 37 deletions
@@ -81,11 +81,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
} }
public reset(): void { public reset(): void {
for (const pending of this._pendingTransmissions.values()) { this._cleanupAllPending();
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._lastPendingKey = undefined;
if (this._activeDecoder) { if (this._activeDecoder) {
this._activeDecoder.release(); this._activeDecoder.release();
this._activeDecoder = null; this._activeDecoder = null;
@@ -97,6 +93,21 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
this.reset(); this.reset();
} }
private _removePendingEntry(key: number): void {
this._pendingTransmissions.delete(key);
if (this._lastPendingKey === key) {
this._lastPendingKey = undefined;
}
}
private _cleanupAllPending(): void {
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._lastPendingKey = undefined;
}
public start(): void { public start(): void {
this._aborted = false; this._aborted = false;
this._decodeError = false; this._decodeError = false;
@@ -178,10 +189,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
} }
this._activeDecoder = null; this._activeDecoder = null;
if (pending) { if (pending) {
this._pendingTransmissions.delete(pendingKey); this._removePendingEntry(pendingKey);
if (this._lastPendingKey === pendingKey) {
this._lastPendingKey = undefined;
}
} }
this._aborted = true; this._aborted = true;
return; return;
@@ -202,10 +210,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
this._activeDecoder = null; this._activeDecoder = null;
this._decodeError = true; this._decodeError = true;
if (pending) { if (pending) {
this._pendingTransmissions.delete(pendingKey); this._removePendingEntry(pendingKey);
if (this._lastPendingKey === pendingKey) {
this._lastPendingKey = undefined;
}
} }
} }
} }
@@ -404,14 +409,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
return true; return true;
} }
const pendingKey = cmd.id ?? 0;
this._handleTransmit(cmd, bytes, decodeError); this._handleTransmit(cmd, bytes, decodeError);
// If still accumulating chunks, don't display yet
if (this._pendingTransmissions.has(pendingKey)) return true;
// Display the completed image
const id = cmd.id ?? this._kittyStorage.lastImageId; const id = cmd.id ?? this._kittyStorage.lastImageId;
const image = this._kittyStorage.getImage(id); const image = this._kittyStorage.getImage(id);
if (image) { if (image) {
@@ -488,26 +487,17 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
switch (selector) { switch (selector) {
case 'a': case 'a':
case 'A': case 'A':
// Delete all — also abort all in-flight uploads this._cleanupAllPending();
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._lastPendingKey = undefined;
this._kittyStorage.deleteAll(); this._kittyStorage.deleteAll();
break; break;
case 'i': case 'i':
case 'I': case 'I':
// Delete by image ID — only abort the targeted upload
if (cmd.id !== undefined) { if (cmd.id !== undefined) {
const pending = this._pendingTransmissions.get(cmd.id); const pending = this._pendingTransmissions.get(cmd.id);
if (pending) { if (pending) {
pending.decoder.release(); pending.decoder.release();
this._pendingTransmissions.delete(cmd.id);
if (this._lastPendingKey === cmd.id) {
this._lastPendingKey = undefined;
}
} }
this._removePendingEntry(cmd.id);
this._kittyStorage.deleteById(cmd.id); this._kittyStorage.deleteById(cmd.id);
} }
break; break;
@@ -574,6 +564,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const zIndex = cmd.zIndex ?? 0; const zIndex = cmd.zIndex ?? 0;
if (w !== bitmap.width || h !== bitmap.height) { if (w !== bitmap.width || h !== bitmap.height) {
const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h }); const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
bitmap.close();
this._kittyStorage.addImage(image.id, resized, true, layer, zIndex); this._kittyStorage.addImage(image.id, resized, true, layer, zIndex);
} else { } else {
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex); this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
@@ -615,7 +606,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
canvas.getContext('2d')?.drawImage(img, 0, 0); canvas.getContext('2d')?.drawImage(img, 0, 0);
createImageBitmap(canvas).then(resolve).catch(reject); createImageBitmap(canvas).then(resolve).catch(reject);
}); });
img.addEventListener('error', reject); img.addEventListener('error', () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load image'));
});
img.src = url; img.src = url;
}); });
} }
@@ -21,16 +21,16 @@ export class KittyImageStorage implements IDisposable {
private _nextImageId = 1; private _nextImageId = 1;
private readonly _images: Map<number, IKittyImageData> = new Map(); private readonly _images: Map<number, IKittyImageData> = new Map();
private readonly _kittyIdToStorageId: Map<number, number> = new Map(); private readonly _kittyIdToStorageId: Map<number, number> = new Map();
private readonly _storageIdToKittyId: Map<number, number> = new Map();
private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined; private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined;
private readonly _wrappedOnImageDeleted: (storageId: number) => void; private readonly _wrappedOnImageDeleted: (storageId: number) => void;
private readonly _handleStorageImageDeleted = (storageId: number): void => { private readonly _handleStorageImageDeleted = (storageId: number): void => {
for (const [kittyId, mappedStorageId] of this._kittyIdToStorageId) { const kittyId = this._storageIdToKittyId.get(storageId);
if (mappedStorageId === storageId) { if (kittyId !== undefined) {
this._kittyIdToStorageId.delete(kittyId); this._kittyIdToStorageId.delete(kittyId);
this._images.delete(kittyId); this._storageIdToKittyId.delete(storageId);
break; this._images.delete(kittyId);
}
} }
}; };
@@ -49,6 +49,7 @@ export class KittyImageStorage implements IDisposable {
this._nextImageId = 1; this._nextImageId = 1;
this._images.clear(); this._images.clear();
this._kittyIdToStorageId.clear(); this._kittyIdToStorageId.clear();
this._storageIdToKittyId.clear();
} }
public dispose(): void { public dispose(): void {
@@ -65,6 +66,7 @@ export class KittyImageStorage implements IDisposable {
if (oldStorageId !== undefined) { if (oldStorageId !== undefined) {
this._storage.deleteImage(oldStorageId); this._storage.deleteImage(oldStorageId);
this._kittyIdToStorageId.delete(imageId); this._kittyIdToStorageId.delete(imageId);
this._storageIdToKittyId.delete(oldStorageId);
} }
if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) { if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) {
@@ -81,6 +83,7 @@ export class KittyImageStorage implements IDisposable {
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void { public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
const storageId = this._storage.addImage(image, scrolling, layer, zIndex); const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
this._kittyIdToStorageId.set(kittyId, storageId); this._kittyIdToStorageId.set(kittyId, storageId);
this._storageIdToKittyId.set(storageId, kittyId);
} }
public getImage(kittyId: number): IKittyImageData | undefined { public getImage(kittyId: number): IKittyImageData | undefined {
@@ -93,6 +96,7 @@ export class KittyImageStorage implements IDisposable {
if (storageId !== undefined) { if (storageId !== undefined) {
this._storage.deleteImage(storageId); this._storage.deleteImage(storageId);
this._kittyIdToStorageId.delete(kittyId); this._kittyIdToStorageId.delete(kittyId);
this._storageIdToKittyId.delete(storageId);
} }
} }
@@ -102,6 +106,7 @@ export class KittyImageStorage implements IDisposable {
this._storage.deleteImage(storageId); this._storage.deleteImage(storageId);
} }
this._kittyIdToStorageId.clear(); this._kittyIdToStorageId.clear();
this._storageIdToKittyId.clear();
} }
public get images(): ReadonlyMap<number, IKittyImageData> { public get images(): ReadonlyMap<number, IKittyImageData> {