@web5/agent
Version:
111 lines (91 loc) • 3.71 kB
text/typescript
import type { JsonRpcResponse } from './json-rpc.js';
import type { DwnRpc, DwnRpcRequest, DwnRpcResponse } from './dwn-rpc-types.js';
import { createJsonRpcRequest, parseJson } from './json-rpc.js';
import { CryptoUtils } from '@web5/crypto';
import { DwnServerInfoCache, ServerInfo } from './server-info-types.js';
import { DwnServerInfoCacheMemory } from './dwn-server-info-cache-memory.js';
/**
* HTTP client that can be used to communicate with Dwn Servers
*/
export class HttpDwnRpcClient implements DwnRpc {
private serverInfoCache: DwnServerInfoCache;
constructor(serverInfoCache?: DwnServerInfoCache) {
this.serverInfoCache = serverInfoCache ?? new DwnServerInfoCacheMemory();
}
get transportProtocols() { return ['http:', 'https:']; }
async sendDwnRequest(request: DwnRpcRequest): Promise<DwnRpcResponse> {
const requestId = CryptoUtils.randomUuid();
const jsonRpcRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', {
target : request.targetDid,
message : request.message
});
const fetchOpts = {
method : 'POST',
headers : {
'dwn-request': JSON.stringify(jsonRpcRequest)
}
};
if (request.data) {
// @ts-expect-error TODO: REMOVE
fetchOpts.headers['content-type'] = 'application/octet-stream';
// @ts-expect-error TODO: REMOVE
fetchOpts['body'] = request.data;
}
const resp = await fetch(request.dwnUrl, fetchOpts);
let dwnRpcResponse: JsonRpcResponse;
// check to see if response is in header first. if it is, that means the response is a ReadableStream
let dataStream;
const { headers } = resp;
if (headers.has('dwn-response')) {
// @ts-expect-error TODO: REMOVE
const jsonRpcResponse = parseJson(headers.get('dwn-response')) as JsonRpcResponse;
if (jsonRpcResponse == null) {
throw new Error(`failed to parse json rpc response. dwn url: ${request.dwnUrl}`);
}
dataStream = resp.body;
dwnRpcResponse = jsonRpcResponse;
} else {
// TODO: wonder if i need to try/catch this?
const responseBody = await resp.text();
dwnRpcResponse = JSON.parse(responseBody);
}
if (dwnRpcResponse.error) {
const { code, message } = dwnRpcResponse.error;
throw new Error(`(${code}) - ${message}`);
}
const { reply } = dwnRpcResponse.result;
if (dataStream && reply.record) {
reply.record.data = dataStream;
} else if (dataStream && reply.entry) {
reply.entry.data = dataStream;
}
return reply as DwnRpcResponse;
}
async getServerInfo(dwnUrl: string): Promise<ServerInfo> {
const serverInfo = await this.serverInfoCache.get(dwnUrl);
if (serverInfo) {
return serverInfo;
}
const url = new URL(dwnUrl);
// add `/info` to the dwn server url path
url.pathname.endsWith('/') ? url.pathname += 'info' : url.pathname += '/info';
try {
const response = await fetch(url.toString());
if(response.ok) {
const results = await response.json() as ServerInfo;
// explicitly return and cache only the desired properties.
const serverInfo = {
registrationRequirements : results.registrationRequirements,
maxFileSize : results.maxFileSize,
webSocketSupport : results.webSocketSupport,
};
this.serverInfoCache.set(dwnUrl, serverInfo);
return serverInfo;
} else {
throw new Error(`HTTP (${response.status}) - ${response.statusText}`);
}
} catch(error: any) {
throw new Error(`Error encountered while processing response from ${url.toString()}: ${error.message}`);
}
}
}