Skip to content
Merged
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
5 changes: 2 additions & 3 deletions packages/client-config-builder/src/ClientConfigBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
StaticResolver,
ResolutionResultCache,
ResolutionResultCacheResolver,
PackageToWrapperResolver,
RequestSynchronizerResolver,
} from "@polywrap/uri-resolvers-js";
import { ExtendableUriResolver } from "@polywrap/uri-resolver-extensions-js";
Expand Down Expand Up @@ -50,15 +49,15 @@ export class ClientConfigBuilder extends BaseClientConfigBuilder {
RecursiveResolver.from(
RequestSynchronizerResolver.from(
ResolutionResultCacheResolver.from(
PackageToWrapperResolver.from([
[
StaticResolver.from([
...this.buildRedirects(),
...this.buildWrappers(),
...this.buildPackages(),
]),
...this._config.resolvers,
new ExtendableUriResolver(),
]),
],
resolutionResultCache ?? new ResolutionResultCache()
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,8 @@ describe("Error structure", () => {
if (result.ok) throw Error("should never happen");

expect(result.error?.name).toEqual("WrapError");
expect(result.error?.code).toEqual(WrapErrorCode.URI_RESOLVER_ERROR);
expect(result.error?.code).toEqual(WrapErrorCode.CLIENT_LOAD_WRAPPER_ERROR);
expect(result.error?.uri.endsWith("tmp")).toBeTruthy();
expect(result.error?.resolutionStack).toBeDefined();
expect(`${result.error?.cause}`).toContain(
`Unrecognized WrapManifest schema version "0.0.0.5"`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe("URI resolution", () => {
);

if (expectResult.ok) {
expectResult.value.type = "wrapper";
expectResult.value.type = "package";
}

await expectResolutionResult(
Expand Down
1 change: 1 addition & 0 deletions packages/uri-resolver-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"devDependencies": {
"@polywrap/cli-js": "0.10.0",
"@polywrap/client-js": "0.10.0",
"@polywrap/core-client-js": "0.10.0",
"@polywrap/plugin-js": "0.10.0",
"@polywrap/test-cases": "0.10.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { Result, ResultErr } from "@polywrap/result";
// $start: UriResolverExtensionFileReader
/** An IFileReader that reads files by invoking URI Resolver Extension wrappers */
export class UriResolverExtensionFileReader implements IFileReader /* $ */ {
private _fileCache: Map<
string,
Promise<Result<Uint8Array, Error>>
> = new Map();

// $start: UriResolverExtensionFileReader-constructor
/**
* Construct a UriResolverExtensionFileReader
Expand All @@ -37,30 +42,58 @@ export class UriResolverExtensionFileReader implements IFileReader /* $ */ {
* */
async readFile(filePath: string): Promise<Result<Uint8Array, Error>> /* $ */ {
const path = combinePaths(this._wrapperUri.path, filePath);
const result = await UriResolverInterface.module.getFile(
{
invoke: <TData = unknown>(
options: InvokeOptions
): Promise<InvokeResult<TData>> => this._client.invoke<TData>(options),
invokeWrapper: <TData = unknown>(
options: InvokeOptions & { wrapper: Wrapper }
): Promise<InvokeResult<TData>> =>
this._client.invokeWrapper<TData>(options),
},
this._resolverExtensionUri,
path
);
if (!result.ok) return result;
if (!result.value) {
return ResultErr(
new Error(
`File not found at ${path} using resolver ${this._resolverExtensionUri.uri}`
)
);

// If the file has already been requested
const existingFile = this._fileCache.get(path);

if (existingFile) {
return existingFile;
}
return {
value: result.value,
ok: true,
};

// else, create a new read file request
const getFileRequest = new Promise<Result<Uint8Array, Error>>((resolve) => {
return UriResolverInterface.module
.getFile(
{
invoke: <TData = unknown>(
options: InvokeOptions
): Promise<InvokeResult<TData>> =>
this._client.invoke<TData>(options),
invokeWrapper: <TData = unknown>(
options: InvokeOptions & { wrapper: Wrapper }
): Promise<InvokeResult<TData>> =>
this._client.invokeWrapper<TData>(options),
},
this._resolverExtensionUri,
path
)
.then((result) => {
if (!result.ok) {
// The UriResolver has encountered an error,
// return the error & reset the file cache (enabling retries).
this._fileCache.delete(path);
resolve(result);
} else if (!result.value) {
// The UriResolver did not find the file @ the provided URI.
resolve(
ResultErr(
new Error(
`File not found at ${path} using resolver ${this._resolverExtensionUri.uri}`
)
)
);
} else {
// The file has been found.
resolve({
value: result.value,
ok: true,
});
}
});
});

this._fileCache.set(path, getFileRequest);

return getFileRequest;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { UriResolverExtensionFileReader } from "../UriResolverExtensionFileReader";

import {
PolywrapClient,
ClientConfigBuilder,
Uri
} from "@polywrap/client-js";
import {
PluginModule,
PluginWrapper
} from "@polywrap/plugin-js";

const mockUriResolverExtUri = "wrap://mock/uri-resolver-ext";
const mockFile = Uint8Array.from([0, 1, 2]);

class MockUriResolverExt extends PluginModule<{}, {}> {
callCount: number = 0;

getCallCount() {
return this.callCount;
}

getFile(args: { path: string }) {
++this.callCount;
if (args.path.includes("throw/now")) {
throw Error("failed during read file");
} else if (args.path.includes("not/found")) {
return null;
}
return mockFile;
}
}

function createMockClient(): PolywrapClient {
const config = new ClientConfigBuilder()
.addWrapper(
mockUriResolverExtUri,
new PluginWrapper(
{ version: "0.1", type: "plugin", name: "counter", abi: {} },
new MockUriResolverExt({})
)
)
.build();
return new PolywrapClient(config);
}

function createUriResolverExtensionFileReader(
mockClient: PolywrapClient
): UriResolverExtensionFileReader {
return new UriResolverExtensionFileReader(
Uri.from(mockUriResolverExtUri),
Uri.from("wrap://foo/bar"),
mockClient
);
}

async function getCallCount(client: PolywrapClient): Promise<number> {
const result = await client.invoke<number>({
uri: mockUriResolverExtUri,
method: "getCallCount"
});

if (!result.ok) throw result.error;
return result.value;
}

describe("UriResolverExtensionFileReader", () => {
it("resolves a file", async () => {
const mockClient = createMockClient();
const fileReader = createUriResolverExtensionFileReader(mockClient);

const file = await fileReader.readFile("some/path");
expect(file.ok).toBeTruthy();
if (!file.ok) throw file.error;
expect(file.value).toMatchObject(mockFile);
});

it("caches files", async () => {
const mockClient = createMockClient();
const fileReader = createUriResolverExtensionFileReader(mockClient);

const file1 = await fileReader.readFile("some/path");
expect(file1.ok).toBeTruthy();
if (!file1.ok) throw file1.error;
expect(file1.value).toMatchObject(mockFile);

// Ensure the call counter is 1
expect(await getCallCount(mockClient)).toBe(1);

// Call again
const file2 = await fileReader.readFile("some/path");
expect(file2.ok).toBeTruthy();
if (!file2.ok) throw file2.error;
expect(file2.value).toMatchObject(mockFile);

// Ensure the call counter is still 1
expect(await getCallCount(mockClient)).toBe(1);
});

it("can synchronize parallel requests", async () => {
const mockClient = createMockClient();
const fileReader = createUriResolverExtensionFileReader(mockClient);

const results = await Promise.all([
fileReader.readFile("some/path"),
fileReader.readFile("some/path")
]);

for (const result of results) {
expect(result.ok).toBeTruthy();
if (!result.ok) throw result.error;
expect(result.value).toMatchObject(mockFile);
}

// Ensure the call counter is 1
expect(await getCallCount(mockClient)).toBe(1);
});

it("can retry when an error is thrown", async () => {
const mockClient = createMockClient();
const fileReader = createUriResolverExtensionFileReader(mockClient);

// It returns an error
const file1 = await fileReader.readFile("throw/now");
expect(file1.ok).toBeFalsy();

// Ensure the call counter is 1
expect(await getCallCount(mockClient)).toBe(1);

// Call again
const file2 = await fileReader.readFile("throw/now");
expect(file2.ok).toBeFalsy();

// Ensure the call counter is now 2
expect(await getCallCount(mockClient)).toBe(2);
});

it("caches result when not found", async () => {
const mockClient = createMockClient();
const fileReader = createUriResolverExtensionFileReader(mockClient);

const file1 = await fileReader.readFile("not/found");
expect(file1.ok).toBeFalsy();

// Ensure the call counter is 1
expect(await getCallCount(mockClient)).toBe(1);

// Call again
const file2 = await fileReader.readFile("not/found");
expect(file2.ok).toBeFalsy();

// Ensure the call counter is still 1
expect(await getCallCount(mockClient)).toBe(1);
});
});