store events in SortedMap to avoid sort on render
This commit is contained in:
@ -53,7 +53,6 @@ const Timeline = (props: TimelineProps) => {
|
|||||||
return followDistance === props.followDistance;
|
return followDistance === props.followDistance;
|
||||||
};
|
};
|
||||||
const a = [...nts.filter(a => a.kind !== EventKind.LiveEvent)];
|
const a = [...nts.filter(a => a.kind !== EventKind.LiveEvent)];
|
||||||
props.noSort || a.sort((a, b) => b.created_at - a.created_at);
|
|
||||||
return a
|
return a
|
||||||
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
|
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
|
||||||
.filter(a => (props.ignoreModeration || !isEventMuted(a)) && checkFollowDistance(a));
|
.filter(a => (props.ignoreModeration || !isEventMuted(a)) && checkFollowDistance(a));
|
||||||
|
@ -43,8 +43,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
|
|||||||
const chains = new Map<u256, Array<TaggedNostrEvent>>();
|
const chains = new Map<u256, Array<TaggedNostrEvent>>();
|
||||||
if (feed.thread) {
|
if (feed.thread) {
|
||||||
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 => {
|
.forEach(v => {
|
||||||
const replyTo = replyChainKey(v);
|
const replyTo = replyChainKey(v);
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
|
85
packages/shared/src/SortedMap/SortedMap.test.ts
Normal file
85
packages/shared/src/SortedMap/SortedMap.test.ts
Normal 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");
|
150
packages/shared/src/SortedMap/SortedMap.tsx
Normal file
150
packages/shared/src/SortedMap/SortedMap.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -5,3 +5,4 @@ export * from "./work-queue";
|
|||||||
export * from "./feed-cache";
|
export * from "./feed-cache";
|
||||||
export * from "./invoices";
|
export * from "./invoices";
|
||||||
export * from "./dexie-like";
|
export * from "./dexie-like";
|
||||||
|
export * from "./SortedMap/SortedMap";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { appendDedupe } from "@snort/shared";
|
import { appendDedupe, SortedMap } from "@snort/shared";
|
||||||
import { EventExt, EventType, TaggedNostrEvent, u256 } from ".";
|
import { EventExt, EventType, TaggedNostrEvent, u256 } from ".";
|
||||||
import { findTag } from "./utils";
|
import { findTag } from "./utils";
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ export class FlatNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
|||||||
*/
|
*/
|
||||||
export class KeyedReplaceableNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
export class KeyedReplaceableNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
||||||
#keyFn: (ev: TaggedNostrEvent) => string;
|
#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) {
|
constructor(fn: (ev: TaggedNostrEvent) => string) {
|
||||||
super();
|
super();
|
||||||
|
Reference in New Issue
Block a user