@waku/core
Version:
TypeScript implementation of the Waku v2 protocol
161 lines (137 loc) • 4.58 kB
text/typescript
import type { PeerId } from "@libp2p/interface";
import {
IDecodedMessage,
IDecoder,
Libp2p,
QueryRequestParams
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import all from "it-all";
import * as lp from "it-length-prefixed";
import { pipe } from "it-pipe";
import { Uint8ArrayList } from "uint8arraylist";
import { StreamManager } from "../stream_manager/index.js";
import { toProtoMessage } from "../to_proto_message.js";
import {
DEFAULT_PAGE_SIZE,
MAX_PAGE_SIZE,
MAX_TIME_RANGE,
StoreQueryRequest,
StoreQueryResponse
} from "./rpc.js";
const log = new Logger("store");
export const StoreCodec = "/vac/waku/store-query/3.0.0";
export class StoreCore {
private readonly streamManager: StreamManager;
public readonly multicodec = StoreCodec;
public constructor(libp2p: Libp2p) {
this.streamManager = new StreamManager(StoreCodec, libp2p.components);
}
public get maxTimeLimit(): number {
return MAX_TIME_RANGE;
}
public async *queryPerPage<T extends IDecodedMessage>(
queryOpts: QueryRequestParams,
decoders: Map<string, IDecoder<T>>,
peerId: PeerId
): AsyncGenerator<Promise<T | undefined>[]> {
if (queryOpts.timeStart && queryOpts.timeEnd) {
const timeDiff =
queryOpts.timeEnd.getTime() - queryOpts.timeStart.getTime();
if (timeDiff > MAX_TIME_RANGE) {
throw new Error("Time range bigger than 24h");
}
}
// Only validate decoder content topics for content-filtered queries
const isHashQuery =
queryOpts.messageHashes && queryOpts.messageHashes.length > 0;
if (
!isHashQuery &&
queryOpts.contentTopics &&
queryOpts.contentTopics.toString() !==
Array.from(decoders.keys()).toString()
) {
throw new Error(
"Internal error, the decoders should match the query's content topics"
);
}
let currentCursor = queryOpts.paginationCursor;
while (true) {
const storeQueryRequest = StoreQueryRequest.create({
...queryOpts,
paginationCursor: currentCursor
});
log.info("Sending store query request:", {
hasMessageHashes: !!queryOpts.messageHashes?.length,
messageHashCount: queryOpts.messageHashes?.length,
pubsubTopic: queryOpts.pubsubTopic,
contentTopics: queryOpts.contentTopics
});
let stream;
try {
stream = await this.streamManager.getStream(peerId);
} catch (e) {
log.error("Failed to get stream", e);
break;
}
const res = await pipe(
[storeQueryRequest.encode()],
lp.encode,
stream,
lp.decode,
async (source) => await all(source)
);
const bytes = new Uint8ArrayList();
res.forEach((chunk) => {
bytes.append(chunk);
});
const storeQueryResponse = StoreQueryResponse.decode(bytes);
if (
!storeQueryResponse.statusCode ||
storeQueryResponse.statusCode >= 300
) {
const errorMessage = `Store query failed with status code: ${storeQueryResponse.statusCode}, description: ${storeQueryResponse.statusDesc}`;
log.error(errorMessage);
throw new Error(errorMessage);
}
if (!storeQueryResponse.messages || !storeQueryResponse.messages.length) {
log.warn("Stopping pagination due to empty messages in response");
break;
}
log.info(
`${storeQueryResponse.messages.length} messages retrieved from store`
);
const decodedMessages = storeQueryResponse.messages.map((protoMsg) => {
if (!protoMsg.message) {
return Promise.resolve(undefined);
}
const contentTopic = protoMsg.message.contentTopic;
if (contentTopic) {
const decoder = decoders.get(contentTopic);
if (decoder) {
return decoder.fromProtoObj(
protoMsg.pubsubTopic || "",
toProtoMessage(protoMsg.message)
);
}
}
return Promise.resolve(undefined);
});
yield decodedMessages;
if (queryOpts.paginationForward) {
currentCursor =
storeQueryResponse.messages[storeQueryResponse.messages.length - 1]
.messageHash;
} else {
currentCursor = storeQueryResponse.messages[0].messageHash;
}
if (
storeQueryResponse.messages.length > MAX_PAGE_SIZE &&
storeQueryResponse.messages.length <
(queryOpts.paginationLimit || DEFAULT_PAGE_SIZE)
) {
break;
}
}
}
}