UNPKG

@waku/utils

Version:
108 lines 4.23 kB
import { sha256 } from "@noble/hashes/sha256"; import { concat, utf8ToBytes } from "../../bytes/index.js"; export * from "./type_guards.js"; export * from "./routing_info.js"; export const formatPubsubTopic = (clusterId, shard) => { return `/waku/2/rs/${clusterId}/${shard}`; }; /** * @deprecated will be removed */ export const pubsubTopicToSingleShardInfo = (pubsubTopics) => { const parts = pubsubTopics.split("/"); if (parts.length != 6 || parts[1] !== "waku" || parts[2] !== "2" || parts[3] !== "rs") throw new Error("Invalid pubsub topic"); const clusterId = parseInt(parts[4]); const shard = parseInt(parts[5]); if (isNaN(clusterId) || isNaN(shard)) throw new Error("Invalid clusterId or shard"); return { clusterId, shard }; }; /** * Given a string, will throw an error if it is not formatted as a valid content topic for autosharding based on https://rfc.vac.dev/spec/51/ * @param contentTopic String to validate * @returns Object with each content topic field as an attribute */ export function ensureValidContentTopic(contentTopic) { const parts = contentTopic.split("/"); if (parts.length < 5 || parts.length > 6) { throw Error(`Content topic format is invalid: ${contentTopic}`); } // Validate generation field if present let generation = 0; if (parts.length == 6) { generation = parseInt(parts[1]); if (isNaN(generation)) { throw new Error(`Invalid generation field in content topic: ${contentTopic}`); } if (generation > 0) { throw new Error(`Generation greater than 0 is not supported: ${contentTopic}`); } } // Validate remaining fields const fields = parts.splice(-4); // Validate application field if (fields[0].length == 0) { throw new Error(`Application field cannot be empty: ${contentTopic}`); } // Validate version field if (fields[1].length == 0) { throw new Error(`Version field cannot be empty: ${contentTopic}`); } // Validate topic name field if (fields[2].length == 0) { throw new Error(`Topic name field cannot be empty: ${contentTopic}`); } // Validate encoding field if (fields[3].length == 0) { throw new Error(`Encoding field cannot be empty: ${contentTopic}`); } return { generation, application: fields[0], version: fields[1], topicName: fields[2], encoding: fields[3] }; } /** * Given a string, determines which autoshard index to use for its pubsub topic. * Based on the algorithm described in the RFC: https://rfc.vac.dev/spec/51//#algorithm */ export function contentTopicToShardIndex(contentTopic, numShardsInCluster) { const { application, version } = ensureValidContentTopic(contentTopic); const digest = sha256(concat([utf8ToBytes(application), utf8ToBytes(version)])); const dataview = new DataView(digest.buffer.slice(-8)); return Number(dataview.getBigUint64(0, false) % BigInt(numShardsInCluster)); } export function contentTopicToPubsubTopic(contentTopic, clusterId, numShardsInCluster) { if (!contentTopic) { throw Error("Content topic must be specified"); } const shardIndex = contentTopicToShardIndex(contentTopic, numShardsInCluster); return `/waku/2/rs/${clusterId}/${shardIndex}`; } /** * Given an array of content topics, groups them together by their Pubsub topic as derived using the algorithm for autosharding. * If any of the content topics are not properly formatted, the function will throw an error. */ export function contentTopicsByPubsubTopic(contentTopics, clusterId, networkShards) { const groupedContentTopics = new Map(); for (const contentTopic of contentTopics) { const pubsubTopic = contentTopicToPubsubTopic(contentTopic, clusterId, networkShards); let topics = groupedContentTopics.get(pubsubTopic); if (!topics) { groupedContentTopics.set(pubsubTopic, []); topics = groupedContentTopics.get(pubsubTopic); } topics.push(contentTopic); } return groupedContentTopics; } //# sourceMappingURL=index.js.map