diff --git a/src/access/wac.test.ts b/src/access/wac.test.ts index e3e7989176..92d2be5837 100644 --- a/src/access/wac.test.ts +++ b/src/access/wac.test.ts @@ -2294,6 +2294,16 @@ describe("setAgentAccess", () => { }, "https://some.pod/.acl", ), + ) + // Save the ACL + .mockResolvedValueOnce( + mockResponse( + "", + { + status: 201, + }, + "https://some.pod/resource.acl", + ), ); const result = await setAgentResourceAccess( @@ -3012,6 +3022,16 @@ describe("setGroupResourceAccess", () => { }, "https://some.pod/.acl", ), + ) + // Save the ACL + .mockResolvedValueOnce( + mockResponse( + "", + { + status: 201, + }, + "https://some.pod/resource.acl", + ), ); const result = await setGroupResourceAccess( @@ -3695,6 +3715,16 @@ describe("setPublicResourceAccess", () => { }, "https://some.pod/.acl", ), + ) + // Save the ACL + .mockResolvedValueOnce( + mockResponse( + "", + { + status: 201, + }, + "https://some.pod/resource.acl", + ), ); const result = await setPublicResourceAccess( diff --git a/src/acl/acl.internal.ts b/src/acl/acl.internal.ts index c5a799ed36..54c2775cb8 100644 --- a/src/acl/acl.internal.ts +++ b/src/acl/acl.internal.ts @@ -61,6 +61,11 @@ import { isAcr } from "../acp/acp.internal"; * This (currently internal) function fetches the ACL indicated in the [[WithServerResourceInfo]] * attached to a resource. * + * The resource ACL and the fallback ACL are fetched **in parallel** to reduce + * latency, especially when HTTP/2 multiplexing is in use. If the resource has + * its own ACL, the fallback result is discarded. Errors from the speculative + * fallback fetch are silently caught so they do not affect the happy path. + * * @internal * @param resourceInfo The Resource info with the ACL URL * @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( }; } try { - const resourceAcl = await internal_fetchResourceAcl(resourceInfo, options); + // Fetch resource ACL and fallback ACL in parallel. If the resource has its + // own ACL the fallback result is discarded. This trades a potentially + // unnecessary fallback fetch for eliminating a serial round-trip, which is + // especially beneficial with HTTP/2 multiplexing. + // The fallback is wrapped in a catch so that a failing speculative fetch + // does not break the happy path when the resource ACL exists. + const [resourceAcl, fallbackAcl] = await Promise.all([ + internal_fetchResourceAcl(resourceInfo, options), + internal_fetchFallbackAcl(resourceInfo, options).catch(() => null), + ]); const acl = - resourceAcl === null - ? { - resourceAcl: null, - fallbackAcl: await internal_fetchFallbackAcl(resourceInfo, options), - } - : { resourceAcl, fallbackAcl: null }; + resourceAcl !== null + ? { resourceAcl, fallbackAcl: null } + : { resourceAcl: null, fallbackAcl }; return acl; } catch (e: unknown) { diff --git a/src/acl/acl.test.ts b/src/acl/acl.test.ts index 973aee90a1..58449025f1 100644 --- a/src/acl/acl.test.ts +++ b/src/acl/acl.test.ts @@ -712,9 +712,12 @@ describe("getSolidDatasetWithAcl", () => { .sourceIri, ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); - expect(mockFetch.mock.calls).toHaveLength(2); - expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); - expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); + // The resource ACL and fallback ACL are fetched in parallel, so more than + // 2 calls may be made, but the resource and its ACL are always fetched. + expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2); + const calledUrls = mockFetch.mock.calls.map((c) => c[0]); + expect(calledUrls).toContain("https://some.pod/resource"); + expect(calledUrls).toContain("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => { @@ -929,9 +932,12 @@ describe("getFileWithAcl", () => { .sourceIri, ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); - expect(mockFetch.mock.calls).toHaveLength(2); - expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); - expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); + // The resource ACL and fallback ACL are fetched in parallel, so more than + // 2 calls may be made, but the resource and its ACL are always fetched. + expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2); + const calledUrls = mockFetch.mock.calls.map((c) => c[0]); + expect(calledUrls).toContain("https://some.pod/resource"); + expect(calledUrls).toContain("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => { @@ -1113,9 +1119,12 @@ describe("getResourceInfoWithAcl", () => { .sourceIri, ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); - expect(mockFetch.mock.calls).toHaveLength(2); - expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); - expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); + // The resource ACL and fallback ACL are fetched in parallel, so more than + // 2 calls may be made, but the resource and its ACL are always fetched. + expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(2); + const calledUrls = mockFetch.mock.calls.map((c) => c[0]); + expect(calledUrls).toContain("https://some.pod/resource"); + expect(calledUrls).toContain("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => {