store events in SortedMap to avoid sort on render
Some checks reported errors
continuous-integration/drone/push Build encountered an error

This commit is contained in:
Martti Malmi 2024-01-04 18:11:38 +02:00
parent 47d92fe171
commit 5d259cee95
6 changed files with 239 additions and 5 deletions

View File

@ -53,7 +53,6 @@ const Timeline = (props: TimelineProps) => {
return followDistance === props.followDistance;
};
const a = [...nts.filter(a => a.kind !== EventKind.LiveEvent)];
props.noSort || a.sort((a, b) => b.created_at - a.created_at);
return a
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
.filter(a => (props.ignoreModeration || !isEventMuted(a)) && checkFollowDistance(a));

View File

@ -43,8 +43,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
const chains = new Map<u256, Array<TaggedNostrEvent>>();
if (feed.thread) {
feed.thread
?.sort((a, b) => b.created_at - a.created_at)
.filter(a => !isBlocked(a.pubkey))
?.filter(a => !isBlocked(a.pubkey))
.forEach(v => {
const replyTo = replyChainKey(v);
if (replyTo) {

View File

@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import RBSortedMap from "../../../../tests/RBSortedMap.ts";
import SortedMap from "./SortedMap.tsx";
function runTestsForMap(MapConstructor: any, mapName: string) {
describe(mapName, () => {
it("should maintain order based on keys when no custom comparator is provided", () => {
const map = new MapConstructor();
map.set(5, "five");
map.set(3, "three");
map.set(8, "eight");
const first = map.first();
const last = map.last();
expect(first).toEqual([3, "three"]);
expect(last).toEqual([8, "eight"]);
});
it("should maintain order based on custom comparator", () => {
const comparator = (a: [string, number], b: [string, number]) => a[1] - b[1];
const map = new MapConstructor(undefined, comparator);
map.set("a", 5);
map.set("b", 3);
map.set("c", 8);
const first = map.first();
const last = map.last();
expect(first).toEqual(["b", 3]);
expect(last).toEqual(["c", 8]);
});
it("should get correct value by key", () => {
const map = new MapConstructor();
map.set(5, "five");
const value = map.get(5);
expect(value).toBe("five");
});
it("should delete entry by key", () => {
const map = new MapConstructor();
map.set(5, "five");
expect(map.has(5)).toBe(true);
map.delete(5);
expect(map.has(5)).toBe(false);
});
it("should iterate in order", () => {
const map = new MapConstructor();
map.set(5, "five");
map.set(3, "three");
map.set(8, "eight");
const entries: [number, string][] = [];
for (const entry of map.entries()) {
entries.push(entry);
}
expect(entries).toEqual([
[3, "three"],
[5, "five"],
[8, "eight"],
]);
});
it("should give correct size", () => {
const map = new MapConstructor();
map.set(5, "five");
map.set(3, "three");
expect(map.size).toBe(2);
});
});
}
// Run the tests for both map implementations.
runTestsForMap(SortedMap, "SortedMap");
runTestsForMap(RBSortedMap, "RBSortedMap");

View File

@ -0,0 +1,150 @@
type Comparator<K, V> = (a: [K, V], b: [K, V]) => number;
export class SortedMap<K, V> {
private map: Map<K, V>;
private sortedKeys: K[];
private compare: Comparator<K, V>;
constructor(initialEntries?: Iterable<readonly [K, V]>, compare?: string | Comparator<K, V>) {
this.map = new Map(initialEntries || []);
if (compare) {
if (typeof compare === "string") {
this.compare = (a, b) => (a[1][compare] > b[1][compare] ? 1 : a[1][compare] < b[1][compare] ? -1 : 0);
} else {
this.compare = compare;
}
} else {
this.compare = (a, b) => (a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0);
}
this.sortedKeys = initialEntries ? [...this.map.entries()].sort(this.compare).map(([key]) => key) : [];
}
private binarySearch(key: K, value: V): number {
let left = 0;
let right = this.sortedKeys.length;
while (left < right) {
const mid = (left + right) >> 1;
const midKey = this.sortedKeys[mid];
const midValue = this.map.get(midKey) as V;
if (this.compare([key, value], [midKey, midValue]) < 0) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
set(key: K, value: V) {
const exists = this.map.has(key);
this.map.set(key, value);
if (exists) {
const index = this.sortedKeys.indexOf(key);
if (index !== -1) {
this.sortedKeys.splice(index, 1);
}
}
const insertAt = this.binarySearch(key, value);
this.sortedKeys.splice(insertAt, 0, key);
}
get(key: K): V | undefined {
return this.map.get(key);
}
last(): [K, V] | undefined {
if (this.sortedKeys.length === 0) {
return undefined;
}
const key = this.sortedKeys[this.sortedKeys.length - 1];
return [key, this.map.get(key) as V];
}
first(): [K, V] | undefined {
if (this.sortedKeys.length === 0) {
return undefined;
}
const key = this.sortedKeys[0];
return [key, this.map.get(key) as V];
}
*[Symbol.iterator](): Iterator<[K, V]> {
for (const key of this.sortedKeys) {
yield [key, this.map.get(key) as V];
}
}
*reverse(): Iterator<[K, V]> {
for (let i = this.sortedKeys.length - 1; i >= 0; i--) {
const key = this.sortedKeys[i];
yield [key, this.map.get(key) as V];
}
}
*keys(): IterableIterator<K> {
for (const key of this.sortedKeys) {
yield key;
}
}
*values(): IterableIterator<V> {
for (const key of this.sortedKeys) {
yield this.map.get(key) as V;
}
}
*entries(): IterableIterator<[K, V]> {
for (const key of this.sortedKeys) {
yield [key, this.map.get(key) as V];
}
}
*range(
options: {
gte?: K;
lte?: K;
direction?: "asc" | "desc";
} = {},
): IterableIterator<[K, V]> {
const { gte, lte, direction = "asc" } = options;
const startIndex = gte ? this.binarySearch(gte, this.map.get(gte) as V) : 0;
const endIndex = lte ? this.binarySearch(lte, this.map.get(lte) as V) : this.sortedKeys.length;
if (direction === "asc") {
for (let i = startIndex; i < endIndex; i++) {
const key = this.sortedKeys[i];
yield [key, this.map.get(key) as V];
}
} else {
for (let i = endIndex - 1; i >= startIndex; i--) {
const key = this.sortedKeys[i];
yield [key, this.map.get(key) as V];
}
}
}
has(key: K): boolean {
return this.map.has(key);
}
delete(key: K): boolean {
if (this.map.delete(key)) {
const index = this.sortedKeys.indexOf(key);
if (index !== -1) {
this.sortedKeys.splice(index, 1);
}
return true;
}
return false;
}
get size(): number {
return this.map.size;
}
}

View File

@ -5,3 +5,4 @@ export * from "./work-queue";
export * from "./feed-cache";
export * from "./invoices";
export * from "./dexie-like";
export * from "./SortedMap/SortedMap";

View File

@ -1,4 +1,4 @@
import { appendDedupe } from "@snort/shared";
import { appendDedupe, SortedMap } from "@snort/shared";
import { EventExt, EventType, TaggedNostrEvent, u256 } from ".";
import { findTag } from "./utils";
@ -196,7 +196,7 @@ export class FlatNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
*/
export class KeyedReplaceableNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
#keyFn: (ev: TaggedNostrEvent) => string;
#events: Map<string, TaggedNostrEvent> = new Map();
#events: SortedMap<string, TaggedNostrEvent> = new SortedMap([], (a, b) => b[1].created_at - a[1].created_at);
constructor(fn: (ev: TaggedNostrEvent) => string) {
super();