From 6239b49aba1c4d8090aa9f963e88d421b0e278e1 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 23 May 2026 20:33:25 +1000 Subject: [PATCH] fix friends freeze --- src/widgets/buttons.ts | 13 +++++- test/unit/widgets/buttons.test.ts | 69 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/widgets/buttons.ts b/src/widgets/buttons.ts index 41ac758c5..51c459e03 100644 --- a/src/widgets/buttons.ts +++ b/src/widgets/buttons.ts @@ -880,6 +880,7 @@ export type attachmentListOptions = { noun?: string renderSupportingInfo?: RenderSupportingInfo renderNameSuffix?: RenderNameSuffix + refreshOnDocumentLoad?: boolean } /** @@ -892,6 +893,11 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML // options = options || {} const docsWaitingForRowRefresh = new Set() const hasAsyncEnrichedRowOptions = !!(options.renderSupportingInfo || options.renderNameSuffix) + // Keep the generic default on for simple consumers: if row decoration depends on + // linked-profile data arriving later, attachmentList will rerender once that + // target document finishes loading. Complex callers with their own streaming or + // batched refresh pipeline can opt out to avoid duplicate whole-list refreshes. + const refreshOnDocumentLoad = options.refreshOnDocumentLoad ?? true const deleteAttachment = function (target) { if (!kb.updater) { @@ -918,13 +924,16 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML opt.renderSupportingInfo = options.renderSupportingInfo opt.renderNameSuffix = options.renderNameSuffix - if (hasAsyncEnrichedRowOptions && target?.uri && kb.fetcher) { + if (hasAsyncEnrichedRowOptions && refreshOnDocumentLoad && target?.uri && kb.fetcher) { const targetDoc = target.doc() const requestState = targetDoc?.uri ? kb.fetcher.requested?.[targetDoc.uri] : undefined const shouldWaitForFetch = requestState !== 'done' && requestState !== 'failed' if (targetDoc?.uri && shouldWaitForFetch && !docsWaitingForRowRefresh.has(targetDoc.uri)) { docsWaitingForRowRefresh.add(targetDoc.uri) - // Root fix: these row options can depend on async profile data, so rerender once fetch completes. + // The row renderer may need data from the target profile that is not loaded yet. + // Register one follow-up refresh per target document so simple attachmentList + // consumers eventually show the enriched row contents without building their own + // async refresh orchestration. kb.fetcher.nowOrWhenFetched(targetDoc, undefined, () => { docsWaitingForRowRefresh.delete(targetDoc.uri) refresh() diff --git a/test/unit/widgets/buttons.test.ts b/test/unit/widgets/buttons.test.ts index c4f6203d2..2c14303e1 100644 --- a/test/unit/widgets/buttons.test.ts +++ b/test/unit/widgets/buttons.test.ts @@ -107,6 +107,11 @@ describe('askName', () => { }) describe('attachmentList', () => { + afterEach(() => { + clearStore() + jest.restoreAllMocks() + }) + it('exists', () => { expect(attachmentList).toBeInstanceOf(Function) }) @@ -116,6 +121,70 @@ describe('attachmentList', () => { const options = {} expect(attachmentList(dom, subject, div, options)).toBeTruthy() }) + + it('refreshes rows after pending profile fetches by default', () => { + const subject = sym('https://subject.example/profile/card#me') + const predicate = ns.foaf('knows') + const target = sym('https://friend.example/profile/card#me') + const div = element + const fetcher = store.fetcher as any + + store.add(subject, predicate, target, subject.doc()) + fetcher.requested = { + ...fetcher.requested, + [target.doc().uri]: 'requested' + } + + const nowOrWhenFetched = jest + .spyOn(fetcher, 'nowOrWhenFetched') + .mockImplementation(() => undefined as any) + + attachmentList(dom, subject, div, { + predicate, + renderSupportingInfo: () => null + }) + + expect(nowOrWhenFetched).toHaveBeenCalledWith( + target.doc(), + undefined, + expect.any(Function) + ) + }) + + it('adds one extra document-load refresh hook only when enabled', () => { + const subject = sym('https://subject.example/profile/card#me') + const predicate = ns.foaf('knows') + const target = sym('https://friend.example/profile/card#me') + const div = element + const fetcher = store.fetcher as any + + store.add(subject, predicate, target, subject.doc()) + fetcher.requested = { + ...fetcher.requested, + [target.doc().uri]: 'requested' + } + + const nowOrWhenFetched = jest + .spyOn(fetcher, 'nowOrWhenFetched') + .mockImplementation(() => undefined as any) + + attachmentList(dom, subject, div, { + predicate, + refreshOnDocumentLoad: false, + renderSupportingInfo: () => null + }) + const disabledCallCount = nowOrWhenFetched.mock.calls.length + + nowOrWhenFetched.mockClear() + div.innerHTML = '' + + attachmentList(dom, subject, div, { + predicate, + renderSupportingInfo: () => null + }) + + expect(nowOrWhenFetched.mock.calls.length).toBe(disabledCallCount + 1) + }) }) describe('button', () => {