Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: replace getObject for pageData with multiGetObjects #270

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions portal/common/lib/page_fetching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export async function resolveObjectId(
if (!objectId) {
// Check if there is a SuiNs name
try {
// TODO: only check for SuiNs names if the subdomain is not a valid base36 string.
objectId = await resolveSuiNsAddress(client, parsedUrl.subdomain);
if (!objectId) {
return noObjectIdFound();
Expand Down
5 changes: 2 additions & 3 deletions portal/common/lib/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { DomainDetails } from "./types/index";
import { getDomain } from "./domain_parsing";
import { aggregatorEndpoint } from "./aggregator";
import { SuiClient } from "@mysten/sui/client";
import { SuiObjectResponse } from "@mysten/sui/client";

/**
* Redirects to the portal URL.
Expand All @@ -29,8 +29,7 @@ export function redirectToAggregatorUrlResponse(scope: URL, blobId: string): Res
/**
* Checks if the object has a redirect in its Display representation.
*/
export async function checkRedirect(client: SuiClient, objectId: string): Promise<string | null> {
const object = await client.getObject({ id: objectId, options: { showDisplay: true } });
export function checkRedirect(object: SuiObjectResponse): string | null {
if (object.data && object.data.display) {
let display = object.data.display;
// Check if "walrus site address" is set in the display field.
Expand Down
133 changes: 69 additions & 64 deletions portal/common/lib/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import { SuiClient } from "@mysten/sui/client";
import { HttpStatusCodes } from "./http/http_status_codes";
import { checkRedirect } from "./redirects";
import { fromBase64 } from "@mysten/bcs";
import { DynamicFieldStruct } from "./bcs_data_parsing";

// Mock SuiClient methods
const getObject = vi.fn();
const multiGetObjects = vi.fn();
const mockClient = {
getObject,
multiGetObjects,
} as unknown as SuiClient;

vi.mock("@mysten/sui/utils", () => ({
deriveDynamicFieldID: vi.fn(() => "0xdynamicFieldId"),
}))

// Mock checkRedirect
vi.mock("./redirects", () => ({
checkRedirect: vi.fn(),
Expand Down Expand Up @@ -61,99 +64,98 @@ describe("fetchResource", () => {

test("should fetch resource without redirect", async () => {
// Mock object response
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
multiGetObjects.mockResolvedValueOnce([
{
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
},
});
{
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
},
]);
(fromBase64 as any).mockReturnValueOnce("decodedBcsBytes");

const result = await fetchResource(mockClient, "0x1", "/path", new Set());
expect(result).toEqual({
blob_id: "0xresourceBlobId",
objectId: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
objectId: "0xdynamicFieldId",
version: undefined,
});
expect(mockClient.getObject).toHaveBeenCalledWith({
id: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
options: { showBcs: true },
});
expect(checkRedirect).toHaveBeenCalledTimes(1);
});

test("should follow redirect and recursively fetch resource", async () => {
// Mock the redirect check to return a redirect ID on the first call
(checkRedirect as any).mockResolvedValueOnce(
"0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096",
);
(checkRedirect as any).mockResolvedValueOnce(undefined);

// Mock the first resource object response
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
const mockObject = {
"objectId": "0x26dc2460093a9d6d31b58cb0ed1e72b19d140542a49be7472a6f25d542cb5cc3",
"version": "150835605",
"digest": "DDD7ZZvLkBQjq1kJpRsPDMpqhvYtGM878SdCTfF42ywE",
"display": {
"data": {
"walrus site address": "0x2"
},
"error": null
},
});

// Mock the final resource object response
getObject.mockResolvedValueOnce({
"bcs": {
"dataType": "moveObject",
"type": "0x1::flatland::Flatlander",
"hasPublicTransfer": true,
"version": 150835605,
}
};

const mockResource = {
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});
};

const result = await fetchResource(mockClient, "0x1", "/path", new Set());
(checkRedirect as any).mockResolvedValueOnce(undefined);

multiGetObjects
.mockResolvedValueOnce([mockObject, mockResource])
.mockResolvedValueOnce([mockObject, mockResource]);

const result = await fetchResource(
mockClient,
"0x26dc2460093a9d6d31b58cb0ed1e72b19d140542a49be7472a6f25d542cb5cc3",
"/path",
new Set());

// Verify the results
expect(result).toEqual({
blob_id: "0xresourceBlobId",
objectId: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
objectId: "0xdynamicFieldId",
version: undefined,
});
expect(checkRedirect).toHaveBeenCalledTimes(1);
expect(checkRedirect).toHaveBeenCalledTimes(2);
});

test("should return NOT_FOUND if the resource does not contain a blob_id", async () => {
const seenResources = new Set<string>();
const mockResource = {}; // No blob_id

(checkRedirect as any).mockResolvedValueOnce(
"0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096",
);

// Mock getObject to return a valid BCS object
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
(checkRedirect as any).mockResolvedValueOnce(undefined);
multiGetObjects.mockReturnValue([
{
data: {
bcs: {
dataType: "moveObject",
},
},
},
});

// Mock fromBase64 to simulate the decoding process
(fromBase64 as any).mockReturnValueOnce("decodedBcsBytes");

// Mock DynamicFieldStruct to return a resource without a blob_id
(DynamicFieldStruct as any).mockImplementation(() => ({
parse: () => ({ value: mockResource }),
}));
{},
]);
(fromBase64 as any).mockReturnValueOnce(undefined);

const result = await fetchResource(mockClient, "0x1", "/path", seenResources);

Expand All @@ -168,7 +170,10 @@ describe("fetchResource", () => {
(checkRedirect as any).mockResolvedValueOnce(null);

// Mock to simulate that dynamic fields are not found
getObject.mockResolvedValueOnce(undefined);
multiGetObjects.mockReturnValue([
{data: {bcs: {dataType: "moveObject"}}},
{},
]);

const result = await fetchResource(mockClient, "0x1", "/path", seenResources);

Expand Down
101 changes: 77 additions & 24 deletions portal/common/lib/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { HttpStatusCodes } from "./http/http_status_codes";
import { SuiClient, SuiObjectData } from "@mysten/sui/client";
import { SuiClient, SuiObjectData, SuiObjectResponse } from "@mysten/sui/client";
import { Resource, VersionedResource } from "./types";
import { MAX_REDIRECT_DEPTH, RESOURCE_PATH_MOVE_TYPE } from "./constants";
import { checkRedirect } from "./redirects";
Expand Down Expand Up @@ -36,51 +36,104 @@ export async function fetchResource(
seenResources: Set<string>,
depth: number = 0,
): Promise<VersionedResource | HttpStatusCodes> {
if (seenResources.has(objectId)) {
return HttpStatusCodes.LOOP_DETECTED;
}
if (depth >= MAX_REDIRECT_DEPTH) {
return HttpStatusCodes.TOO_MANY_REDIRECTS;
}

const redirectPromise = checkRedirect(client, objectId);
seenResources.add(objectId);
const error = checkForErrors(objectId, seenResources, depth);
if (error) return error;

// The dynamic field object ID can be derived, without
// making a request to the network.
const dynamicFieldId = deriveDynamicFieldID(
objectId,
RESOURCE_PATH_MOVE_TYPE,
bcs.string().serialize(path).toBytes(),
);
console.log("Derived dynamic field objectID: ", dynamicFieldId);

// Fetch page data.
const pageData = await client.getObject({
id: dynamicFieldId,
options: { showBcs: true },
});
const [
primaryObjectResponse,
dynamicFieldResponse
] = await fetchObjectPairData(client, objectId, dynamicFieldId);

seenResources.add(objectId);

const redirectId = checkRedirect(primaryObjectResponse);
if (redirectId) {
return fetchResource(client, redirectId, path, seenResources, depth + 1);
}

return extractResource(dynamicFieldResponse, dynamicFieldId);
}

// If no page data found.
if (!pageData || !pageData.data) {
const redirectId = await redirectPromise;
if (redirectId) {
return fetchResource(client, redirectId, path, seenResources, depth + 1);
}
/**
* Fetches the data of a parentObject and its' dynamicFieldObject.
* @param client: A SuiClient to interact with the Sui network.
* @param objectId: The objectId of the parentObject (e.g. site::Site).
* @param dynamicFieldId: The Id of the dynamicFieldObject (e.g. site::Resource).
* @returns A tuple of SuiObjectResponse[] or an HttpStatusCode in case of an error.
*/
async function fetchObjectPairData(
client: SuiClient,
objectId: string,
dynamicFieldId: string
): Promise<SuiObjectResponse[]> {
// MultiGetObjects returns the objects *always* in the order they were requested.
const pageData = await client.multiGetObjects(
{
ids: [
objectId,
dynamicFieldId
],
options: { showBcs: true, showDisplay: true }
},
);
// MultiGetObjects returns the objects *always* in the order they were requested.
const primaryObjectResponse: SuiObjectResponse = pageData[0];
const dynamicFieldResponse: SuiObjectResponse = pageData[1];

return [primaryObjectResponse, dynamicFieldResponse]
}

/**
* Extracts the resource data from the dynamicFieldObject.
* @param dynamicFieldResponse: contains the data of the dynamicFieldObject
* @param dynamicFieldId: The Id of the dynamicFieldObject (e.g. site::Resource).
* @returns A VersionedResource or an HttpStatusCode in case of an error.
*/
function extractResource(
dynamicFieldResponse: SuiObjectResponse,
dynamicFieldId: string): VersionedResource | HttpStatusCodes
{
if (!dynamicFieldResponse.data) {
console.log("No page data found");
return HttpStatusCodes.NOT_FOUND;
}

const siteResource = getResourceFields(pageData.data);
const siteResource = getResourceFields(dynamicFieldResponse.data);
if (!siteResource || !siteResource.blob_id) {
return HttpStatusCodes.NOT_FOUND;
}

return {
...siteResource,
version: pageData.data?.version,
version: dynamicFieldResponse.data.version,
objectId: dynamicFieldId,
} as VersionedResource;
}

/**
* Checks for loop detection and too many redirects.
* @param objectId
* @param seenResources
* @param depth
* @returns
*/
function checkForErrors(
objectId: string,
seenResources: Set<string>, depth: number
): HttpStatusCodes | null {
if (seenResources.has(objectId)) return HttpStatusCodes.LOOP_DETECTED;
if (depth >= MAX_REDIRECT_DEPTH) return HttpStatusCodes.TOO_MANY_REDIRECTS;
return null;
}

/**
* Parses the resource information from the Sui object data response.
*/
Expand Down
Loading
Loading