@web5/agent
Version:
100 lines (79 loc) • 3.98 kB
text/typescript
import type { DwnRpc, DwnRpcRequest, DwnRpcResponse, DwnSubscriptionHandler } from './dwn-rpc-types.js';
import type { GenericMessage, MessageSubscription, UnionMessageReply } from '@tbd54566975/dwn-sdk-js';
import { CryptoUtils } from '@web5/crypto';
import { createJsonRpcRequest, createJsonRpcSubscriptionRequest } from './json-rpc.js';
import { JsonRpcSocket, JsonRpcSocketOptions } from './json-rpc-socket.js';
interface SocketConnection {
socket: JsonRpcSocket;
subscriptions: Map<string, MessageSubscription>;
}
export class WebSocketDwnRpcClient implements DwnRpc {
public get transportProtocols() { return ['ws:', 'wss:']; }
// a map of dwn host to WebSocket connection
private static connections = new Map<string, SocketConnection>();
async sendDwnRequest(request: DwnRpcRequest, jsonRpcSocketOptions?: JsonRpcSocketOptions): Promise<DwnRpcResponse> {
// validate that the dwn URL provided is a valid WebSocket URL
const url = new URL(request.dwnUrl);
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
throw new Error(`Invalid websocket protocol ${url.protocol}`);
}
// check if there is already a connection to this host, if it does not exist, initiate a new connection
const hasConnection = WebSocketDwnRpcClient.connections.has(url.host);
if (!hasConnection) {
try {
const socket = await JsonRpcSocket.connect(url.toString(), jsonRpcSocketOptions);
const subscriptions = new Map();
WebSocketDwnRpcClient.connections.set(url.host, { socket, subscriptions });
} catch(error) {
throw new Error(`Error connecting to ${url.host}: ${(error as Error).message}`);
}
}
const connection = WebSocketDwnRpcClient.connections.get(url.host)!;
const { targetDid, message, subscriptionHandler } = request;
if (subscriptionHandler) {
return WebSocketDwnRpcClient.subscriptionRequest(connection, targetDid, message, subscriptionHandler);
}
return WebSocketDwnRpcClient.processMessage(connection, targetDid, message);
}
private static async processMessage(connection: SocketConnection, target: string, message: GenericMessage): Promise<DwnRpcResponse> {
const requestId = CryptoUtils.randomUuid();
const request = createJsonRpcRequest(requestId, 'dwn.processMessage', { target, message });
const { socket } = connection;
const response = await socket.request(request);
const { error, result } = response;
if (error !== undefined) {
throw new Error(`error sending DWN request: ${error.message}`);
}
return result.reply as DwnRpcResponse;
}
private static async subscriptionRequest(connection: SocketConnection, target:string, message: GenericMessage, messageHandler: DwnSubscriptionHandler): Promise<DwnRpcResponse> {
const requestId = CryptoUtils.randomUuid();
const subscriptionId = CryptoUtils.randomUuid();
const request = createJsonRpcSubscriptionRequest(requestId, 'dwn.processMessage', subscriptionId, { target, message });
const { socket, subscriptions } = connection;
const { response, close } = await socket.subscribe(request, (response) => {
const { result, error } = response;
if (error) {
// if there is an error, close the subscription and delete it from the connection
const subscription = subscriptions.get(subscriptionId);
if (subscription) {
subscription.close();
}
subscriptions.delete(subscriptionId);
return;
}
const { event } = result;
messageHandler(event);
});
const { error, result } = response;
if (error) {
throw new Error(`could not subscribe via jsonrpc socket: ${error.message}`);
}
const { reply } = result as { reply: UnionMessageReply };
if (reply.subscription && close) {
subscriptions.set(subscriptionId, { ...reply.subscription, close });
reply.subscription.close = close;
}
return reply;
}
}