UNPKG

@web5/agent

Version:
181 lines (151 loc) 6.45 kB
import type { DidUrlDereferencer } from '@web5/dids'; import { Jws, PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js'; import { Readable } from '@web5/common'; import { utils as didUtils } from '@web5/dids'; import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'; import { DateSort, DwnInterfaceName, DwnMethodName, Message, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; export function blobToIsomorphicNodeReadable(blob: Blob): Readable { return webReadableToIsomorphicNodeReadable(blob.stream() as ReadableStream<any>); } export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: DidUrlDereferencer): Promise<string[]> { // Attempt to dereference the DID service with ID fragment #dwn. const dereferencingResult = await dereferencer.dereference(`${didUri}#dwn`); if (dereferencingResult.dereferencingMetadata.error) { throw new Error(`Failed to dereference '${didUri}#dwn': ${dereferencingResult.dereferencingMetadata.error}`); } if (didUtils.isDwnDidService(dereferencingResult.contentStream)) { const { serviceEndpoint } = dereferencingResult.contentStream; const serviceEndpointUrls = typeof serviceEndpoint === 'string' // If the service endpoint is a string, format it as a single-element array. ? [serviceEndpoint] : Array.isArray(serviceEndpoint) && serviceEndpoint.every(endpoint => typeof endpoint === 'string') // If the service endpoint is an array of strings, use it as is. ? serviceEndpoint as string[] // If the service endpoint is neither a string nor an array of strings, return an empty array. : []; if (serviceEndpointUrls.length > 0) { return serviceEndpointUrls; } } // If the DID service with ID fragment #dwn was not found or is not valid, return an empty array. return []; } export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { return Message.getAuthor(record); } /** * Get the `protocolRole` string from the signature payload of the given RecordsWriteMessage or RecordsDeleteMessage. */ export function getRecordProtocolRole(message: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); return signaturePayload?.protocolRole; } export function isRecordsWrite(obj: unknown): obj is RecordsWrite { // Validate that the given value is an object. if (!obj || typeof obj !== 'object' || obj === null) return false; // Validate that the object has the necessary properties of RecordsWrite. return ( 'message' in obj && typeof obj.message === 'object' && obj.message !== null && 'descriptor' in obj.message && typeof obj.message.descriptor === 'object' && obj.message.descriptor !== null && 'interface' in obj.message.descriptor && obj.message.descriptor.interface === DwnInterfaceName.Records && 'method' in obj.message.descriptor && obj.message.descriptor.method === DwnMethodName.Write ); } /** * Get the CID of the given RecordsWriteMessage. */ export function getRecordMessageCid(message: RecordsWriteMessage): Promise<string> { return Message.getCid(message); } /** * Get the pagination cursor for the given RecordsWriteMessage and DateSort. * * @param message The RecordsWriteMessage for which to get the pagination cursor. * @param dateSort The date sort that will be used in the query or subscription to which the cursor will be applied. */ export async function getPaginationCursor(message: RecordsWriteMessage, dateSort: DateSort): Promise<PaginationCursor> { const value = dateSort === DateSort.CreatedAscending || dateSort === DateSort.CreatedDescending ? message.descriptor.dateCreated : message.descriptor.datePublished; if (value === undefined) { throw new Error('The dateCreated or datePublished property is missing from the record descriptor.'); } return { messageCid: await getRecordMessageCid(message), value }; } export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream<any>) { return new ReadableWebToNodeStream(webReadable); } /** * Polling function with interval, TTL accepting a custom fetch function * @template T - the return you expect from the fetcher * @param fetchFunction an http fetch function * @param [interval=3000] how frequently to poll * @param [ttl=300_000] how long until polling stops * @returns T - the result of fetch */ export function pollWithTtl( fetchFunction: () => Promise<Response>, interval = 3000, ttl = 300_000, abortSignal?: AbortSignal ): Promise<Response | null> { const endTime = Date.now() + ttl; let timeoutId: NodeJS.Timeout | null = null; let isPolling = true; return new Promise((resolve, reject) => { if (abortSignal) { abortSignal.addEventListener('abort', () => { isPolling = false; if (timeoutId !== null) { clearTimeout(timeoutId); } console.log('Polling aborted by user'); resolve(null); }); } async function poll() { if (!isPolling) return; const remainingTime = endTime - Date.now(); if (remainingTime <= 0) { isPolling = false; console.log('Polling stopped: TTL reached'); resolve(null); return; } console.log(`Polling... (Remaining time: ${Math.ceil(remainingTime / 1000)}s)`); try { const response = await fetchFunction(); if (response.ok) { isPolling = false; if (timeoutId !== null) { clearTimeout(timeoutId); } console.log('Polling stopped: Success condition met'); resolve(response); return; } } catch (error) { console.error('Error fetching data:', error); reject(error); } if (isPolling) { timeoutId = setTimeout(poll, interval); } } poll(); }); } /** Concatenates a base URL and a path ensuring that there is exactly one slash between them */ export function concatenateUrl(baseUrl: string, path: string): string { // Remove trailing slash from baseUrl if it exists if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } // Remove leading slash from path if it exists if (path.startsWith('/')) { path = path.slice(1); } return `${baseUrl}/${path}`; }