Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/access/wac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
25 changes: 18 additions & 7 deletions src/acl/acl.internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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) {
Expand Down
27 changes: 18 additions & 9 deletions src/acl/acl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading