Skip to content

Commit 86d086f

Browse files
pgodwincursoragent
andcommitted
Cut AFP icon traffic and make file activity a floating queued transfer list.
Skip fork reads when type/creator is already cached, parse only the resource map plus needed icons, and cap concurrent AFP tasks. Transfers now queue nested expand jobs and paint in a movable activity window instead of a callout. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 981ed6f commit 86d086f

22 files changed

Lines changed: 1240 additions & 369 deletions

src/fs/icon-cache.test.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { describe, expect, it } from 'vitest';
22
import { ResourceFork, type ResourceEntry } from './resource-fork';
3-
import { iconSetForFile, isCdevStyleType } from './icon-cache';
4-
import { CDEV_ICON_ID, IconSize } from './resource-types/icon-set';
3+
import {
4+
HAS_BUNDLE,
5+
HAS_CUSTOM_ICON,
6+
IconCache,
7+
iconForkLoadOptions,
8+
iconSetForFile,
9+
isCdevStyleType,
10+
shouldReadIconFork,
11+
} from './icon-cache';
12+
import { CDEV_ICON_ID, CUSTOM_ICON_ID, IconSize } from './resource-types/icon-set';
13+
import type { VNode } from './virtual-fs';
514

615
function entry(type: string, id: number, payload: Uint8Array): ResourceEntry {
716
return {
@@ -22,12 +31,14 @@ function icn(): Uint8Array {
2231
return data;
2332
}
2433

25-
function finder(type: string, creator: string): Uint8Array {
34+
function finder(type: string, creator: string, flags = 0): Uint8Array {
2635
const fi = new Uint8Array(32);
2736
for (let i = 0; i < 4; i++) {
2837
fi[i] = type.charCodeAt(i) ?? 0x20;
2938
fi[4 + i] = creator.charCodeAt(i) ?? 0x20;
3039
}
40+
fi[8] = (flags >> 8) & 0xff;
41+
fi[9] = flags & 0xff;
3142
return fi;
3243
}
3344

@@ -92,3 +103,69 @@ describe('iconSetForFile', () => {
92103
expect(set?.getIconBySize(IconSize.Large, true, false)?.typeCode).toBe('icl8');
93104
});
94105
});
106+
107+
describe('shouldReadIconFork', () => {
108+
it('skips ordinary documents with no bundle or custom-icon flag', () => {
109+
expect(shouldReadIconFork(finder('TEXT', 'ttxt'), 'TEXT')).toBe(false);
110+
});
111+
112+
it('reads APPL, cdev, bundle-bit, and custom-icon files', () => {
113+
expect(shouldReadIconFork(finder('APPL', 'TEST'), 'APPL')).toBe(true);
114+
expect(shouldReadIconFork(finder('cdev', 'Rver'), 'cdev')).toBe(true);
115+
expect(shouldReadIconFork(finder('TEXT', 'ttxt', HAS_BUNDLE), 'TEXT')).toBe(true);
116+
expect(shouldReadIconFork(finder('TEXT', 'ttxt', HAS_CUSTOM_ICON), 'TEXT')).toBe(true);
117+
});
118+
119+
it('skips a bundled app once its type/creator is cached', () => {
120+
const cached = { small: 'data:image/png,x', large: 'data:image/png,x' };
121+
expect(shouldReadIconFork(finder('APPL', 'TEST'), 'APPL', cached)).toBe(false);
122+
expect(
123+
shouldReadIconFork(finder('APPL', 'TEST'), 'APPL', {
124+
small: '/icons/APPL16.png',
125+
large: '/icons/APPL32.png',
126+
}),
127+
).toBe(false);
128+
});
129+
});
130+
131+
describe('iconForkLoadOptions', () => {
132+
it('asks for the custom-icon family on Icon\\r files', () => {
133+
const node: VNode = {
134+
id: 1,
135+
parentId: 2,
136+
name: 'Icon\r',
137+
isDir: false,
138+
data: new Uint8Array(),
139+
resource: new Uint8Array(),
140+
finderInfo: finder('icon', 'MACS', HAS_CUSTOM_ICON),
141+
createDate: 0,
142+
modDate: 0,
143+
};
144+
const opts = iconForkLoadOptions(node);
145+
expect(opts.includeAllIcons).toBe(true);
146+
expect(opts.extraIds).toContain(CUSTOM_ICON_ID);
147+
});
148+
});
149+
150+
describe('IconCache.getForNode', () => {
151+
it('does not look up Icon\\r inside a closed folder', async () => {
152+
const cache = new IconCache();
153+
let probes = 0;
154+
const dir: VNode = {
155+
id: 4,
156+
parentId: 2,
157+
name: 'Folder',
158+
isDir: true,
159+
data: new Uint8Array(),
160+
resource: new Uint8Array(),
161+
finderInfo: new Uint8Array(32),
162+
createDate: 0,
163+
modDate: 0,
164+
};
165+
await cache.getForNode(dir, undefined, async () => {
166+
probes += 1;
167+
return null;
168+
});
169+
expect(probes).toBe(0);
170+
});
171+
});

src/fs/icon-cache.ts

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function isCustomFolderIconName(name: string): boolean {
3030
const ICON_CACHE_DB = 'classicstack-icon-cache';
3131
/** Bump when decoded-icon preference or extract rules change so stale BW PNGs are dropped. */
3232
const ICON_CACHE_DB_VERSION = 3;
33-
const HAS_CUSTOM_ICON = 0x0400;
33+
/** Finder FileInfo/DInfo: custom icon (Icon\\r or id -16455). */
34+
export const HAS_CUSTOM_ICON = 0x0400;
35+
/** Finder FileInfo: BNDL resources in the file's resource fork. */
36+
export const HAS_BUNDLE = 0x2000;
3437
/** Finder FileInfo/DInfo flag: item is invisible (AppleDouble FinderInfo). */
3538
export const FINDER_IS_INVISIBLE = 0x4000;
3639

@@ -84,6 +87,34 @@ export function isCdevStyleType(type: string): boolean {
8487
return CDEV_STYLE_TYPES.has(padOsType(type));
8588
}
8689

90+
/** True when this file's own resource fork (not a type/creator cache) may hold a Finder icon. */
91+
export function shouldReadIconFork(
92+
finderInfo: Uint8Array,
93+
type: string,
94+
cached?: IconUrls | null,
95+
): boolean {
96+
const flags = finderFlags(finderInfo);
97+
if ((flags & HAS_CUSTOM_ICON) !== 0) return true;
98+
if (cached) return false;
99+
const t = padOsType(type);
100+
return (flags & HAS_BUNDLE) !== 0 || t === 'APPL' || isCdevStyleType(t);
101+
}
102+
103+
/** Ids / extract rules for a ranged AFP resource-fork read. */
104+
export function iconForkLoadOptions(node: VNode): FinderIconForkOpts {
105+
const { type } = readTypeCreator(node.finderInfo);
106+
const flags = finderFlags(node.finderInfo);
107+
const extraIds: number[] = [];
108+
if (isCdevStyleType(type)) extraIds.push(CDEV_ICON_ID);
109+
if ((flags & HAS_CUSTOM_ICON) !== 0 || isCustomFolderIconName(node.name)) extraIds.push(CUSTOM_ICON_ID);
110+
const fid = finderIconId(node.finderInfo);
111+
if (fid) extraIds.push(fid);
112+
return {
113+
extraIds,
114+
includeAllIcons: isCustomFolderIconName(node.name),
115+
};
116+
}
117+
87118
function isSystemIconUrls(urls: IconUrls): boolean {
88119
return urls.small.startsWith('/icons/') || urls.large.startsWith('/icons/');
89120
}
@@ -204,6 +235,8 @@ export class IconCache {
204235
private memory = new Map<string, IconUrls>();
205236
private bundleCache = new Map<string, BundleCacheEntry>();
206237
private dirMemory = new Map<string, IconUrls>();
238+
private typeInflight = new Map<string, Promise<IconUrls>>();
239+
private dirInflight = new Map<string, Promise<IconUrls>>();
207240
private db: IDBPDatabase | null = null;
208241
private initPromise: Promise<void> | null = null;
209242
private systemReady: Promise<void> | null = null;
@@ -241,6 +274,8 @@ export class IconCache {
241274
this.memory.clear();
242275
this.bundleCache.clear();
243276
this.dirMemory.clear();
277+
this.typeInflight.clear();
278+
this.dirInflight.clear();
244279
await this.init();
245280
if (!this.db) return;
246281
await this.clearStore('typeIcons');
@@ -249,6 +284,7 @@ export class IconCache {
249284
/** Drop cached folder icons (e.g. after VirtualFS mutations / Icon\\r changes). */
250285
clearDirectoryCache(): void {
251286
this.dirMemory.clear();
287+
this.dirInflight.clear();
252288
}
253289

254290
private async clearStore(store: string): Promise<void> {
@@ -293,8 +329,36 @@ export class IconCache {
293329
return this.getForDirectory(String(node.id), node, findChild, loadIconFork);
294330
}
295331
const { type, creator } = readTypeCreator(node.finderInfo);
332+
const key = cacheKey(creator, type);
333+
const cached = this.memory.get(key) ?? (await this.loadPersisted(key));
334+
if (cached) this.memory.set(key, cached);
335+
const custom = (finderFlags(node.finderInfo) & HAS_CUSTOM_ICON) !== 0;
336+
if (cached && !custom) return cached;
337+
338+
if (!custom) {
339+
const pending = this.typeInflight.get(key);
340+
if (pending) return pending;
341+
}
342+
343+
const work = this.resolveFileNode(node, type, creator, cached, loadIconFork);
344+
if (!custom) {
345+
this.typeInflight.set(key, work);
346+
void work.finally(() => {
347+
if (this.typeInflight.get(key) === work) this.typeInflight.delete(key);
348+
});
349+
}
350+
return work;
351+
}
352+
353+
private async resolveFileNode(
354+
node: VNode,
355+
type: string,
356+
creator: string,
357+
cached: IconUrls | null,
358+
loadIconFork?: (node: VNode) => Promise<ResourceFork | null>,
359+
): Promise<IconUrls> {
296360
let fork: ResourceFork | null = null;
297-
if (node.resource.length < 16 && loadIconFork) {
361+
if (node.resource.length < 16 && loadIconFork && shouldReadIconFork(node.finderInfo, type, cached)) {
298362
try {
299363
fork = await loadIconFork(node);
300364
} catch {
@@ -315,6 +379,8 @@ export class IconCache {
315379
async getForTypeCreator(type: string, creator: string): Promise<IconUrls> {
316380
await this.ensureDefaults();
317381
const key = cacheKey(creator, type);
382+
const pending = this.typeInflight.get(key);
383+
if (pending) return pending;
318384
const cached = this.memory.get(key) ?? (await this.loadPersisted(key));
319385
if (cached) {
320386
this.memory.set(key, cached);
@@ -337,6 +403,36 @@ export class IconCache {
337403
const hit = this.dirMemory.get(pathKey);
338404
if (hit) return hit;
339405

406+
// Only probe Icon\\r / the directory fork when the caller listed this
407+
// folder (findChild). Closed folders keep the default glyph until opened.
408+
if (!findChild) {
409+
return (
410+
this.defaultFolder ?? {
411+
small: systemIconUrl('DIR16.png'),
412+
large: systemIconUrl('DIR32.png'),
413+
}
414+
);
415+
}
416+
417+
const pending = this.dirInflight.get(pathKey);
418+
if (pending) return pending;
419+
const work = this.probeDirectory(pathKey, node, findChild, loadIconFork);
420+
this.dirInflight.set(pathKey, work);
421+
void work.finally(() => {
422+
if (this.dirInflight.get(pathKey) === work) this.dirInflight.delete(pathKey);
423+
});
424+
return work;
425+
}
426+
427+
private async probeDirectory(
428+
pathKey: string,
429+
node: VNode,
430+
findChild: (parentId: number, name: string) => Promise<VNode | undefined>,
431+
loadIconFork?: (node: VNode) => Promise<ResourceFork | null>,
432+
): Promise<IconUrls> {
433+
const hit = this.dirMemory.get(pathKey);
434+
if (hit) return hit;
435+
340436
// Classic Finder custom folder icon lives in a root file named "Icon\r"
341437
// (Icon + CR / 0x0D), resource id -16455.
342438
const fromIconFile = await this.tryCustomFolderIconFile(node, findChild, loadIconFork);
@@ -374,7 +470,7 @@ export class IconCache {
374470
small: systemIconUrl('DIR16.png'),
375471
large: systemIconUrl('DIR32.png'),
376472
};
377-
// Do not cache defaults — allows Icon\r added later to be picked up.
473+
this.dirMemory.set(pathKey, urls);
378474
return urls;
379475
}
380476

@@ -419,13 +515,18 @@ export class IconCache {
419515
}): Promise<IconUrls> {
420516
const key = cacheKey(args.creator, args.type);
421517
const rf = forkFromNode(args.resource, args.data, args.fork ?? null);
518+
const custom = (finderFlags(args.finderInfo) & HAS_CUSTOM_ICON) !== 0;
422519
const cached = this.memory.get(key) ?? (await this.loadPersisted(key));
423-
if (cached && !(rf && isSystemIconUrls(cached))) {
520+
if (cached && !custom) {
424521
this.memory.set(key, cached);
425522
return cached;
426523
}
427524

428525
if (!rf) {
526+
if (cached) {
527+
this.memory.set(key, cached);
528+
return cached;
529+
}
429530
const urls: IconUrls = {
430531
small: await resolveSystemIcon(args.type, 16),
431532
large: await resolveSystemIcon(args.type, 32),
@@ -443,8 +544,10 @@ export class IconCache {
443544
if (set) {
444545
const urls = await iconSetToUrls(set);
445546
if (urls) {
446-
this.memory.set(key, urls);
447-
await this.persist(key, urls);
547+
if (!custom) {
548+
this.memory.set(key, urls);
549+
await this.persist(key, urls);
550+
}
448551
return urls;
449552
}
450553
}

0 commit comments

Comments
 (0)