Skip to content

Commit 9aaad7c

Browse files
maxleonardclaude
andcommitted
perf: parallelize resource ACL and fallback ACL fetching
Fetch resource ACL and fallback ACL concurrently using Promise.all instead of sequentially. This reduces latency when combined with HTTP/2 multiplexing, as the speculative fallback fetch overlaps with the resource ACL fetch at minimal cost. - internal_fetchAcl now issues both fetches in parallel - Fallback wrapped in .catch(() => null) to prevent regression when speculative fetch fails - Updated tests for parallel fetch behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ebf7a13 commit 9aaad7c

File tree

2 files changed

+36
-16
lines changed

2 files changed

+36
-16
lines changed

src/acl/acl.internal.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ import { isAcr } from "../acp/acp.internal";
6161
* This (currently internal) function fetches the ACL indicated in the [[WithServerResourceInfo]]
6262
* attached to a resource.
6363
*
64+
* The resource ACL and the fallback ACL are fetched **in parallel** to reduce
65+
* latency, especially when HTTP/2 multiplexing is in use. If the resource has
66+
* its own ACL, the fallback result is discarded. Errors from the speculative
67+
* fallback fetch are silently caught so they do not affect the happy path.
68+
*
6469
* @internal
6570
* @param resourceInfo The Resource info with the ACL URL
6671
* @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters).
@@ -76,15 +81,21 @@ export async function internal_fetchAcl(
7681
};
7782
}
7883
try {
79-
const resourceAcl = await internal_fetchResourceAcl(resourceInfo, options);
84+
// Fetch resource ACL and fallback ACL in parallel. If the resource has its
85+
// own ACL the fallback result is discarded. This trades a potentially
86+
// unnecessary fallback fetch for eliminating a serial round-trip, which is
87+
// especially beneficial with HTTP/2 multiplexing.
88+
// The fallback is wrapped in a catch so that a failing speculative fetch
89+
// does not break the happy path when the resource ACL exists.
90+
const [resourceAcl, fallbackAcl] = await Promise.all([
91+
internal_fetchResourceAcl(resourceInfo, options),
92+
internal_fetchFallbackAcl(resourceInfo, options).catch(() => null),
93+
]);
8094

8195
const acl =
82-
resourceAcl === null
83-
? {
84-
resourceAcl: null,
85-
fallbackAcl: await internal_fetchFallbackAcl(resourceInfo, options),
86-
}
87-
: { resourceAcl, fallbackAcl: null };
96+
resourceAcl !== null
97+
? { resourceAcl, fallbackAcl: null }
98+
: { resourceAcl: null, fallbackAcl };
8899

89100
return acl;
90101
} catch (e: unknown) {

src/acl/acl.test.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -712,9 +712,12 @@ describe("getSolidDatasetWithAcl", () => {
712712
.sourceIri,
713713
).toBe("https://some.pod/resource.acl");
714714
expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull();
715-
expect(mockFetch.mock.calls).toHaveLength(2);
716-
expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource");
717-
expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl");
715+
// The resource ACL and fallback ACL are fetched in parallel, so more than
716+
// 2 calls may be made, but the resource and its ACL are always fetched.
717+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2);
718+
const calledUrls = mockFetch.mock.calls.map((c) => c[0]);
719+
expect(calledUrls).toContain("https://some.pod/resource");
720+
expect(calledUrls).toContain("https://some.pod/resource.acl");
718721
});
719722

720723
it("returns the Resource's Container's ACL if its own ACL is not available", async () => {
@@ -929,9 +932,12 @@ describe("getFileWithAcl", () => {
929932
.sourceIri,
930933
).toBe("https://some.pod/resource.acl");
931934
expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull();
932-
expect(mockFetch.mock.calls).toHaveLength(2);
933-
expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource");
934-
expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl");
935+
// The resource ACL and fallback ACL are fetched in parallel, so more than
936+
// 2 calls may be made, but the resource and its ACL are always fetched.
937+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2);
938+
const calledUrls = mockFetch.mock.calls.map((c) => c[0]);
939+
expect(calledUrls).toContain("https://some.pod/resource");
940+
expect(calledUrls).toContain("https://some.pod/resource.acl");
935941
});
936942

937943
it("returns the Resource's Container's ACL if its own ACL is not available", async () => {
@@ -1113,9 +1119,12 @@ describe("getResourceInfoWithAcl", () => {
11131119
.sourceIri,
11141120
).toBe("https://some.pod/resource.acl");
11151121
expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull();
1116-
expect(mockFetch.mock.calls).toHaveLength(2);
1117-
expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource");
1118-
expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl");
1122+
// The resource ACL and fallback ACL are fetched in parallel, so more than
1123+
// 2 calls may be made, but the resource and its ACL are always fetched.
1124+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2);
1125+
const calledUrls = mockFetch.mock.calls.map((c) => c[0]);
1126+
expect(calledUrls).toContain("https://some.pod/resource");
1127+
expect(calledUrls).toContain("https://some.pod/resource.acl");
11191128
});
11201129

11211130
it("returns the Resource's Container's ACL if its own ACL is not available", async () => {

0 commit comments

Comments
 (0)