diff --git a/examples/local-explorer/packages/client/src/App.tsx b/examples/local-explorer/packages/client/src/App.tsx
index 82c60486a9..db417ece41 100644
--- a/examples/local-explorer/packages/client/src/App.tsx
+++ b/examples/local-explorer/packages/client/src/App.tsx
@@ -1,7 +1,14 @@
import { useMUD } from "./MUDContext";
+import { useEffect } from "react";
const styleUnset = { all: "unset" } as const;
+// Function to generate random task descriptions
+const generateRandomTask = () => {
+ const tasks = ["Buy groceries", "Walk the dog", "Do laundry", "Clean the house", "Pay bills"];
+ return tasks[Math.floor(Math.random() * tasks.length)] + " " + Math.floor(Math.random() * 1000);
+};
+
export const App = () => {
const {
network: { tables, useStore },
@@ -14,6 +21,17 @@ export const App = () => {
return records;
});
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const randomTask = generateRandomTask();
+ addTask(randomTask);
+ }, 5000); // 100ms interval for 10 tasks per second
+
+ // TODO: Add a way to stop the interval (e.g., after a certain number of tasks or time)
+
+ return () => clearInterval(interval);
+ }, [addTask]);
+
return (
<>
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx
index c3b5eacea0..b5775a44be 100644
--- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx
+++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx
@@ -1,10 +1,20 @@
"use client";
import { BoxIcon, CheckCheckIcon, ReceiptTextIcon, UserPenIcon, XIcon } from "lucide-react";
+import { parseAsInteger, useQueryState } from "nuqs";
import React, { useState } from "react";
-import { ExpandedState, flexRender, getCoreRowModel, getExpandedRowModel, useReactTable } from "@tanstack/react-table";
+import {
+ ExpandedState,
+ flexRender,
+ getCoreRowModel,
+ getExpandedRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
import { createColumnHelper } from "@tanstack/react-table";
import { Badge } from "../../../../../../components/ui/Badge";
+import { Button } from "../../../../../../components/ui/Button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../../../components/ui/Select";
import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table";
import { TruncatedHex } from "../../../../../../components/ui/TruncatedHex";
@@ -96,48 +106,103 @@ export const columns = [
export function TransactionsTable() {
const transactions = useTransactionWatcher();
const [expanded, setExpanded] = useState({});
+ const [pageIndex, setPageIndex] = useQueryState("txPage", parseAsInteger.withDefault(0));
+ const [pageSize, setPageSize] = useQueryState("txPageSize", parseAsInteger.withDefault(10));
const table = useReactTable({
data: transactions,
columns,
state: {
expanded,
+ pagination: {
+ pageIndex,
+ pageSize,
+ },
},
getRowId: (row) => row.writeId,
onExpandedChange: setExpanded,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onPaginationChange: (updater) => {
+ // TODO: check this more
+ if (typeof updater === "function") {
+ const newState = updater({ pageIndex, pageSize });
+ setPageIndex(newState.pageIndex);
+ setPageSize(newState.pageSize);
+ }
+ },
});
return (
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => {
- return (
-
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
-
- );
- })}
-
- ))}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => )
- ) : (
-
-
-
- Waiting for
- transactions…
-
-
-
- )}
-
-
+ <>
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
+
+ );
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => )
+ ) : (
+
+
+
+ Waiting
+ for transactions…
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
+
+
+
+
+
+ >
);
}
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts
index 58d56c2a9e..3ace531431 100644
--- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts
+++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts
@@ -16,9 +16,11 @@ import { useConfig, useWatchBlocks } from "wagmi";
import { getTransaction, simulateContract, waitForTransactionReceipt } from "wagmi/actions";
import { useStore } from "zustand";
import { useCallback, useEffect, useMemo, useState } from "react";
+import { observer } from "../../../../../../observer/decorator";
import { Message } from "../../../../../../observer/messages";
import { type Write, store } from "../../../../../../observer/store";
import { useChain } from "../../../../hooks/useChain";
+import { usePrevious } from "../../../../hooks/usePrevious";
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
export type WatchedTransaction = {
@@ -45,6 +47,10 @@ export function useTransactionWatcher() {
const [transactions, setTransactions] = useState([]);
const observerWrites = useStore(store, (state) => state.writes);
+ // const observerWritesLen = observerWrites.length;
+ // const prevObserverWritesLen = usePrevious(observerWrites.length);
+ // const latestUpdatesLen = observerWritesLen - (prevObserverWritesLen || 0);
+
const handleTransaction = useCallback(
async (hash: Hex, timestamp: bigint) => {
if (!abi) return;
@@ -65,7 +71,7 @@ export function useTransactionWatcher() {
functionName = transaction.input.length > 10 ? transaction.input.slice(0, 10) : "unknown";
}
- const write = Object.values(observerWrites).find((write) => write.hash === hash);
+ const write = observerWrites.find((write) => write.hash === hash);
setTransactions((prevTransactions) => [
{
hash,
@@ -133,7 +139,7 @@ export function useTransactionWatcher() {
);
useEffect(() => {
- for (const write of Object.values(observerWrites)) {
+ for (const write of observerWrites.slice(0, 50)) {
const hash = write.hash;
if (write.type === "waitForTransactionReceipt" && hash && write.address === worldAddress) {
const transaction = transactions.find((transaction) => transaction.hash === hash);
@@ -155,42 +161,42 @@ export function useTransactionWatcher() {
pollingInterval: 500,
});
- const mergedTransactions = useMemo((): WatchedTransaction[] => {
- const mergedMap = new Map();
-
- for (const write of Object.values(observerWrites)) {
- if (write.address !== worldAddress) continue;
-
- const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction;
- const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result");
-
- mergedMap.set(write.hash || write.writeId, {
- hash: write.hash,
- writeId: write.writeId,
- from: write.from,
- status: writeResult?.status === "rejected" ? "rejected" : "pending",
- timestamp: BigInt(write.time) / 1000n,
- functionData: {
- functionName: parsedAbiItem.name,
- args: write.args,
- },
- value: write.value,
- error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined,
- write,
- });
- }
-
- for (const transaction of transactions) {
- const existing = mergedMap.get(transaction.hash);
- if (existing) {
- mergedMap.set(transaction.hash, { ...transaction, write: existing.write });
- } else {
- mergedMap.set(transaction.hash, { ...transaction });
- }
- }
-
- return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n));
- }, [observerWrites, worldAddress, transactions]);
-
- return mergedTransactions;
+ // const mergedTransactions = useMemo((): WatchedTransaction[] => {
+ // const mergedMap = new Map();
+
+ // for (const write of observerWrites) {
+ // if (write.address !== worldAddress) continue;
+
+ // const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction;
+ // const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result");
+
+ // mergedMap.set(write.hash || write.writeId, {
+ // hash: write.hash,
+ // writeId: write.writeId,
+ // from: write.from,
+ // status: writeResult?.status === "rejected" ? "rejected" : "pending",
+ // timestamp: BigInt(write.time) / 1000n,
+ // functionData: {
+ // functionName: parsedAbiItem.name,
+ // args: write.args,
+ // },
+ // value: write.value,
+ // error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined,
+ // write,
+ // });
+ // }
+
+ // for (const transaction of transactions) {
+ // const existing = mergedMap.get(transaction.hash);
+ // if (existing) {
+ // mergedMap.set(transaction.hash, { ...transaction, write: existing.write });
+ // } else {
+ // mergedMap.set(transaction.hash, { ...transaction });
+ // }
+ // }
+
+ // return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n));
+ // }, [observerWrites, worldAddress, transactions]);
+
+ return transactions;
}
diff --git a/packages/explorer/src/observer/store.ts b/packages/explorer/src/observer/store.ts
index 19c2196dae..9fa1262ad8 100644
--- a/packages/explorer/src/observer/store.ts
+++ b/packages/explorer/src/observer/store.ts
@@ -21,13 +21,11 @@ export type Write = {
};
export type State = {
- writes: {
- [id: string]: Write;
- };
+ writes: Write[];
};
export const store = createStore(() => ({
- writes: {},
+ writes: [],
}));
debug("listening for relayed messages", relayChannelName);
@@ -35,17 +33,33 @@ const channel = new BroadcastChannel(relayChannelName);
channel.addEventListener("message", ({ data }: MessageEvent) => {
if (data.type === "ping") return;
store.setState((state) => {
- const write = data.type === "write" ? ({ ...data, events: [] } satisfies Write) : state.writes[data.writeId];
+ const writeIndex = state.writes.findIndex((w) => w.writeId === data.writeId);
+ const write =
+ writeIndex !== -1
+ ? state.writes[writeIndex]
+ : data.type === "write"
+ ? ({ ...data, events: [] } satisfies Write)
+ : undefined;
+
+ if (!write) return state;
+
+ const updatedWrite = {
+ ...write,
+ type: data.type,
+ hash: data.type === "waitForTransactionReceipt" ? data.hash : write.hash,
+ events: [...write.events, data],
+ };
+
+ const newWrites = [...state.writes];
+ if (writeIndex !== -1) {
+ newWrites.splice(writeIndex, 1);
+ newWrites.unshift(updatedWrite);
+ } else {
+ newWrites.unshift(updatedWrite);
+ }
+
return {
- writes: {
- ...state.writes,
- [data.writeId]: {
- ...write,
- type: data.type,
- hash: data.type === "waitForTransactionReceipt" ? data.hash : write.hash,
- events: [...write.events, data],
- },
- },
+ writes: newWrites,
};
});
});