From 90c7927283d3333abb4f5c87c2d564132928d2a1 Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Mon, 27 Oct 2025 15:10:23 -0700 Subject: [PATCH] feat: add a control for querying store in dogfooding app (#142) --- examples/dogfooding/index.html | 11 +++ examples/dogfooding/public/style.css | 54 ++++++++++++- examples/dogfooding/src/index.ts | 105 ++++++++++++++++++++++++++ examples/dogfooding/src/ui-manager.ts | 79 +++++++++++++++++++ 4 files changed, 248 insertions(+), 1 deletion(-) diff --git a/examples/dogfooding/index.html b/examples/dogfooding/index.html index 4707cab..696e74c 100644 --- a/examples/dogfooding/index.html +++ b/examples/dogfooding/index.html @@ -51,6 +51,17 @@ +
+

Store Protocol Query

+
+ + + +
+
+
+
+

Discovery Peers Over Time

diff --git a/examples/dogfooding/public/style.css b/examples/dogfooding/public/style.css index 2980905..39723b3 100644 --- a/examples/dogfooding/public/style.css +++ b/examples/dogfooding/public/style.css @@ -92,6 +92,24 @@ h2 { background-color: #2980b9; } +.btn-success { + background-color: #27ae60; + color: white; +} + +.btn-success:hover { + background-color: #229954; +} + +.btn-danger { + background-color: #e74c3c; + color: white; +} + +.btn-danger:hover { + background-color: #c0392b; +} + .search-container { margin-top: 15px; display: flex; @@ -106,6 +124,40 @@ h2 { font-size: 0.9em; } +/* Store Query Section */ +.store-controls { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.store-controls label { + font-weight: 500; + color: #555; +} + +.store-controls input[type="number"] { + width: 80px; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 0.9em; +} + +.store-status { + margin-top: 15px; + padding: 10px; + border-radius: 5px; + min-height: 20px; +} + +.store-results { + margin-top: 15px; + max-height: 500px; + overflow-y: auto; +} + /* Message Display */ .message-list { max-height: 400px; @@ -214,7 +266,7 @@ footer { grid-template-columns: repeat(2, 1fr); } - .message-stats, .message-controls { + .message-stats, .message-controls, .store-query, .charts { grid-column: span 1; } diff --git a/examples/dogfooding/src/index.ts b/examples/dogfooding/src/index.ts index c267b6c..a82ae21 100644 --- a/examples/dogfooding/src/index.ts +++ b/examples/dogfooding/src/index.ts @@ -27,6 +27,9 @@ import { trackMessageSent, trackMessageReceived, recordLatency, + updateStoreQueryStatus, + displayStoreQueryResults, + clearStoreQueryResults, } from "./ui-manager"; const NUM_MESSAGES_PER_BATCH = 5; @@ -206,6 +209,100 @@ async function initializeApp() { console.log("Subscription active."); }; + const queryStoreMessages = async () => { + const storeMessageCountInput = document.getElementById("storeMessageCount") as HTMLInputElement; + const messageLimit = storeMessageCountInput ? parseInt(storeMessageCountInput.value, 10) : 5; + + if (isNaN(messageLimit) || messageLimit < 1) { + updateStoreQueryStatus("Please enter a valid number of messages (minimum 1)", true); + return; + } + + clearStoreQueryResults(); + updateStoreQueryStatus("Querying store...", false); + console.log(`Querying store for up to ${messageLimit} messages...`); + + try { + const decoder = createWakuDecoder(); + const allMessages: ChatMessage[] = []; + + console.log("Decoder content topic:", decoder.contentTopic); + console.log("Decoder pubsub topic:", decoder.pubsubTopic); + + // Query for messages from the last hour, using paginationLimit to control result size + const timeEnd = new Date(); + const timeStart = new Date(Date.now() - 1000 * 60 * 60); + + const queryOptions = { + timeStart, + timeEnd, + paginationForward: false, // Start from newest + paginationLimit: messageLimit, // Limit the number of messages returned + }; + + console.log("Store query options:", queryOptions); + console.log("Time range:", timeStart.toISOString(), "to", timeEnd.toISOString()); + + // Collect messages - stop once we have enough + await node.store.queryWithOrderedCallback( + [decoder], + async (wakuMessage) => { + // Check if we already have enough messages before processing more + if (allMessages.length >= messageLimit) { + console.log(`Already collected ${messageLimit} messages, stopping`); + return true; // Stop processing + } + + const chatMessage = decodeMessage(wakuMessage.payload); + if (chatMessage) { + allMessages.push(chatMessage); + console.log(`Store found message ${allMessages.length}/${messageLimit}:`, { + id: chatMessage.id, + content: chatMessage.content.substring(0, 50), + timestamp: new Date(chatMessage.timestamp).toISOString(), + sender: chatMessage.senderPeerId.substring(0, 12) + }); + + // Stop if we've reached the limit + if (allMessages.length >= messageLimit) { + console.log(`Reached limit of ${messageLimit} messages, stopping`); + return true; // Stop processing + } + } else { + console.warn("Failed to decode message from store"); + } + + return false; // Continue to next message + }, + queryOptions + ); + + console.log(`Store query completed. Collected ${allMessages.length} messages.`); + + if (allMessages.length > 0) { + // Sort by timestamp descending (newest first) + // Since we're querying with paginationForward: false, we're getting recent messages, + // but they may not be in perfect order, so we sort them + allMessages.sort((a, b) => b.timestamp - a.timestamp); + + console.log(`Returning ${allMessages.length} message(s)`); + console.log("Newest message timestamp:", new Date(allMessages[0].timestamp).toISOString()); + if (allMessages.length > 1) { + console.log("Oldest returned message timestamp:", new Date(allMessages[allMessages.length - 1].timestamp).toISOString()); + } + + updateStoreQueryStatus(`✓ Successfully retrieved ${allMessages.length} message${allMessages.length !== 1 ? 's' : ''} from store`, false); + displayStoreQueryResults(allMessages); + } else { + updateStoreQueryStatus("✓ Query completed successfully, but no messages found in store", false); + displayStoreQueryResults([]); + } + } catch (error) { + console.error("Error querying store:", error); + updateStoreQueryStatus(`✗ Error querying store: ${error instanceof Error ? error.message : String(error)}`, true); + } + }; + const sendMessageButton = document.getElementById("sendMessageButton"); if (sendMessageButton) { sendMessageButton.addEventListener("click", () => { @@ -241,6 +338,14 @@ async function initializeApp() { }); } + const queryStoreButton = document.getElementById("queryStoreButton"); + if (queryStoreButton) { + queryStoreButton.addEventListener("click", () => { + console.log("Query Store button clicked"); + queryStoreMessages(); + }); + } + initCharts(); (window as any).onDiscoveryUpdate = onDiscoveryUpdate; (window as any).onConnectionsUpdate = onConnectionsUpdate; diff --git a/examples/dogfooding/src/ui-manager.ts b/examples/dogfooding/src/ui-manager.ts index 04743c8..90ab8a3 100644 --- a/examples/dogfooding/src/ui-manager.ts +++ b/examples/dogfooding/src/ui-manager.ts @@ -322,3 +322,82 @@ export function recordLatency(id: string, sent: number, received?: number) { export function wireUiToggles() { setupCollapsibles(); } + +// Store Query UI +const storeQueryStatusEl = document.getElementById("storeQueryStatus") as HTMLDivElement | null; +const storeQueryResultsEl = document.getElementById("storeQueryResults") as HTMLDivElement | null; + +export function updateStoreQueryStatus(message: string, isError: boolean = false) { + if (!storeQueryStatusEl) return; + storeQueryStatusEl.textContent = message; + storeQueryStatusEl.style.color = isError ? '#d32f2f' : '#2e7d32'; + storeQueryStatusEl.style.fontWeight = 'bold'; + storeQueryStatusEl.style.marginTop = '10px'; +} + +export function displayStoreQueryResults(messages: ChatMessage[]) { + if (!storeQueryResultsEl) return; + + storeQueryResultsEl.innerHTML = ""; + + if (messages.length === 0) { + storeQueryResultsEl.innerHTML = "

No messages found.

"; + return; + } + + const title = document.createElement("h3"); + title.textContent = `Found ${messages.length} message${messages.length !== 1 ? 's' : ''}:`; + title.style.marginTop = "15px"; + storeQueryResultsEl.appendChild(title); + + messages.forEach((message, index) => { + const item = document.createElement("div"); + item.classList.add("message-item"); + item.style.marginBottom = "10px"; + item.style.padding = "10px"; + item.style.border = "1px solid #ddd"; + item.style.borderRadius = "4px"; + item.style.backgroundColor = "#f9f9f9"; + + const indexLabel = document.createElement("p"); + indexLabel.style.fontWeight = "bold"; + indexLabel.style.marginBottom = "5px"; + indexLabel.textContent = `Message ${index + 1}`; + + const idText = document.createElement("p"); + idText.style.fontSize = "0.9em"; + idText.style.color = "#666"; + idText.textContent = `ID: ${message.id}`; + + const contentP = document.createElement("p"); + contentP.style.margin = "8px 0"; + contentP.textContent = message.content; + + const senderInfoP = document.createElement("p"); + senderInfoP.style.fontSize = "0.9em"; + senderInfoP.style.color = "#666"; + senderInfoP.textContent = `From: ${message.senderPeerId.substring(0, 12)}...`; + + const timestampP = document.createElement("p"); + timestampP.style.fontSize = "0.9em"; + timestampP.style.color = "#666"; + timestampP.textContent = `Time: ${new Date(message.timestamp).toLocaleString()}`; + + item.appendChild(indexLabel); + item.appendChild(idText); + item.appendChild(contentP); + item.appendChild(senderInfoP); + item.appendChild(timestampP); + + storeQueryResultsEl.appendChild(item); + }); +} + +export function clearStoreQueryResults() { + if (storeQueryResultsEl) { + storeQueryResultsEl.innerHTML = ""; + } + if (storeQueryStatusEl) { + storeQueryStatusEl.textContent = ""; + } +}