UNPKG

@waku/sdk

Version:

A unified SDK for easy creation and management of js-waku nodes.

258 lines 10.7 kB
import { peerIdFromString } from "@libp2p/peer-id"; import { multiaddr } from "@multiformats/multiaddr"; import { messageHash, StoreCore } from "@waku/core"; import { Protocols } from "@waku/interfaces"; import { isDefined, Logger } from "@waku/utils"; const log = new Logger("store-sdk"); /** * StoreSDK is an implementation of the IStoreSDK interface. * It provides methods to interact with the Waku Store protocol. */ export class Store { options; libp2p; peerManager; protocol; constructor(params) { this.options = params.options || {}; this.peerManager = params.peerManager; this.libp2p = params.libp2p; this.protocol = new StoreCore(params.libp2p); } get multicodec() { return this.protocol.multicodec; } /** * Queries the Waku Store for historical messages using the provided decoders and options. * Returns an asynchronous generator that yields promises of decoded messages. * * @param decoders - An array of message decoders. * @param options - Optional query parameters. * @returns An asynchronous generator of promises of decoded messages. * @throws If no peers are available to query or if an error occurs during the query. */ async *queryGenerator(decoders, options) { const { decodersAsMap, queryOptions } = this.buildQueryParams(decoders, options); for (const queryOption of queryOptions) { const peer = await this.getPeerToUse(queryOption.pubsubTopic); if (!peer) { log.error("No peers available to query"); throw new Error("No peers available to query"); } log.info(`Querying store with options: ${JSON.stringify(queryOption)}`); const responseGenerator = this.protocol.queryPerPage(queryOption, decodersAsMap, peer); for await (const messages of responseGenerator) { yield messages; } } } /** * Queries the Waku Store for historical messages and processes them with the provided callback in order. * * @param decoders - An array of message decoders. * @param callback - A callback function to process each decoded message. * @param options - Optional query parameters. * @returns A promise that resolves when the query and message processing are completed. */ async queryWithOrderedCallback(decoders, callback, options) { log.info("Querying store with ordered callback"); for await (const promises of this.queryGenerator(decoders, options)) { if (await this.processMessages(promises, callback)) break; } } /** * Queries the Waku Store for historical messages and processes them with the provided callback using promises. * * @param decoders - An array of message decoders. * @param callback - A callback function to process each promise of a decoded message. * @param options - Optional query parameters. * @returns A promise that resolves when the query and message processing are completed. */ async queryWithPromiseCallback(decoders, callback, options) { log.info("Querying store with promise callback"); let abort = false; for await (const page of this.queryGenerator(decoders, options)) { const _promises = page.map(async (msgPromise) => { if (abort) return; abort = Boolean(await callback(msgPromise)); }); await Promise.all(_promises); if (abort) break; } } /** * Processes messages based on the provided callback and options. * * @param messages - An array of promises of decoded messages. * @param callback - A callback function to process each decoded message. * @returns A promise that resolves to a boolean indicating whether the processing should abort. * @private */ async processMessages(messages, callback) { let abort = false; const messagesOrUndef = await Promise.all(messages); const processedMessages = messagesOrUndef.filter(isDefined); await Promise.all(processedMessages.map(async (msg) => { if (msg && !abort) { abort = Boolean(await callback(msg)); } })); return abort; } /** * Creates a cursor based on the provided decoded message. * * @param message - The decoded message. * @returns A StoreCursor representing the message. */ createCursor(message) { return messageHash(message.pubsubTopic, message); } /** * Validates the provided decoders and pubsub topic. * * @param decoders - An array of message decoders. * @returns An object containing the pubsub topic, content topics, and a map of decoders. * @throws If no decoders are provided, if multiple pubsub topics are provided, or if no decoders are found for the pubsub topic. * @private */ validateDecodersAndPubsubTopic(decoders) { if (decoders.length === 0) { log.error("No decoders provided"); throw new Error("No decoders provided"); } const uniquePubsubTopicsInQuery = Array.from(new Set(decoders.map((decoder) => decoder.pubsubTopic))); if (uniquePubsubTopicsInQuery.length > 1) { log.error("API does not support querying multiple pubsub topics at once"); throw new Error("API does not support querying multiple pubsub topics at once"); } const pubsubTopicForQuery = uniquePubsubTopicsInQuery[0]; const decodersAsMap = new Map(); decoders.forEach((dec) => { if (decodersAsMap.has(dec.contentTopic)) { log.error("API does not support different decoder per content topic"); throw new Error("API does not support different decoder per content topic"); } decodersAsMap.set(dec.contentTopic, dec); }); const contentTopics = decoders .filter((decoder) => decoder.pubsubTopic === pubsubTopicForQuery) .map((dec) => dec.contentTopic); if (contentTopics.length === 0) { log.error(`No decoders found for topic ${pubsubTopicForQuery}`); throw new Error("No decoders found for topic " + pubsubTopicForQuery); } return { pubsubTopic: pubsubTopicForQuery, contentTopics, decodersAsMap }; } async getPeerToUse(pubsubTopic) { const peers = await this.peerManager.getPeers({ protocol: Protocols.Store, pubsubTopic }); return this.options.peers ? await this.getPeerFromConfigurationOrFirst(peers, this.options.peers) : peers[0]; } async getPeerFromConfigurationOrFirst(peerIds, configPeers) { const storeConfigPeers = configPeers.map(multiaddr); const missing = []; for (const peer of storeConfigPeers) { const matchedPeer = peerIds.find((id) => id.toString() === peer.getPeerId()?.toString()); if (matchedPeer) { return matchedPeer; } missing.push(peer); } while (missing.length) { const toDial = missing.pop(); if (!toDial) { return; } try { const conn = await this.libp2p.dial(toDial); if (conn) { return peerIdFromString(toDial.getPeerId()); } } catch (e) { log.warn(`Failed to dial peer from options.peers list for Store protocol. Peer:${toDial.getPeerId()}, error:${e}`); } } log.warn(`Passed node to use for Store not found: ${configPeers.toString()}. Attempting to use first available peers.`); return peerIds[0]; } buildQueryParams(decoders, options) { // For message hash queries, don't validate decoders but still need decodersAsMap const isHashQuery = options?.messageHashes && options.messageHashes.length > 0; let pubsubTopic; let contentTopics; let decodersAsMap; if (isHashQuery) { // For hash queries, we still need decoders to decode messages // but we don't validate pubsubTopic consistency // Use pubsubTopic from options if provided, otherwise from first decoder pubsubTopic = options.pubsubTopic || decoders[0]?.pubsubTopic || ""; contentTopics = []; decodersAsMap = new Map(); decoders.forEach((dec) => { decodersAsMap.set(dec.contentTopic, dec); }); } else { const validated = this.validateDecodersAndPubsubTopic(decoders); pubsubTopic = validated.pubsubTopic; contentTopics = validated.contentTopics; decodersAsMap = validated.decodersAsMap; } const subTimeRanges = []; if (options?.timeStart && options?.timeEnd) { let start = options.timeStart; const end = options.timeEnd; while (end.getTime() - start.getTime() > this.protocol.maxTimeLimit) { const subEnd = new Date(start.getTime() + this.protocol.maxTimeLimit); subTimeRanges.push([start, subEnd]); start = subEnd; } if (subTimeRanges.length === 0) { log.info("Using single time range"); subTimeRanges.push([start, end]); } } if (subTimeRanges.length === 0) { log.info("No sub time ranges"); return { decodersAsMap, queryOptions: [ { pubsubTopic, contentTopics, includeData: true, paginationForward: true, ...options } ] }; } log.info(`Building ${subTimeRanges.length} sub time ranges`); return { decodersAsMap, queryOptions: subTimeRanges.map(([start, end]) => ({ pubsubTopic, contentTopics, includeData: true, paginationForward: true, ...options, timeStart: start, timeEnd: end })) }; } } //# sourceMappingURL=store.js.map