Skip to content

Commit 6fc7788

Browse files
committed
utils: merging two (reverse) sorted lists of events.
1 parent 2180c7a commit 6fc7788

File tree

2 files changed

+148
-3
lines changed

2 files changed

+148
-3
lines changed

utils.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
insertEventIntoDescendingList,
77
binarySearch,
88
normalizeURL,
9+
mergeReverseSortedLists,
910
} from './utils.ts'
1011

1112
import type { Event } from './core.ts'
@@ -270,6 +271,94 @@ test('binary search', () => {
270271
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('[' < b ? -1 : '[' === b ? 0 : 1))).toEqual([0, false])
271272
})
272273

274+
describe('mergeReverseSortedLists', () => {
275+
test('merge empty lists', () => {
276+
const list1: Event[] = []
277+
const list2: Event[] = []
278+
expect(mergeReverseSortedLists(list1, list2)).toHaveLength(0)
279+
})
280+
281+
test('merge list with empty list', () => {
282+
const list1 = [buildEvent({ id: 'a', created_at: 30 }), buildEvent({ id: 'b', created_at: 20 })]
283+
const list2: Event[] = []
284+
const result = mergeReverseSortedLists(list1, list2)
285+
expect(result).toHaveLength(2)
286+
expect(result.map(e => e.id)).toEqual(['a', 'b'])
287+
})
288+
289+
test('merge two simple lists', () => {
290+
const list1 = [
291+
buildEvent({ id: 'a', created_at: 30 }),
292+
buildEvent({ id: 'b', created_at: 10 }),
293+
buildEvent({ id: 'f', created_at: 3 }),
294+
buildEvent({ id: 'g', created_at: 2 }),
295+
]
296+
const list2 = [
297+
buildEvent({ id: 'c', created_at: 25 }),
298+
buildEvent({ id: 'd', created_at: 5 }),
299+
buildEvent({ id: 'e', created_at: 1 }),
300+
]
301+
const result = mergeReverseSortedLists(list1, list2)
302+
expect(result.map(e => e.id)).toEqual(['a', 'c', 'b', 'd', 'f', 'g', 'e'])
303+
})
304+
305+
test('merge lists with same timestamps', () => {
306+
const list1 = [
307+
buildEvent({ id: 'a', created_at: 30 }),
308+
buildEvent({ id: 'b', created_at: 20 }),
309+
buildEvent({ id: 'f', created_at: 10 }),
310+
]
311+
const list2 = [
312+
buildEvent({ id: 'c', created_at: 30 }),
313+
buildEvent({ id: 'd', created_at: 20 }),
314+
buildEvent({ id: 'e', created_at: 20 }),
315+
]
316+
const result = mergeReverseSortedLists(list1, list2)
317+
expect(result.map(e => e.id)).toEqual(['c', 'a', 'd', 'e', 'b', 'f'])
318+
})
319+
320+
test('deduplicate events with same timestamp and id', () => {
321+
const list1 = [
322+
buildEvent({ id: 'a', created_at: 30 }),
323+
buildEvent({ id: 'b', created_at: 20 }),
324+
buildEvent({ id: 'b', created_at: 20 }),
325+
buildEvent({ id: 'c', created_at: 20 }),
326+
buildEvent({ id: 'd', created_at: 10 }),
327+
]
328+
const list2 = [
329+
buildEvent({ id: 'a', created_at: 30 }),
330+
buildEvent({ id: 'c', created_at: 20 }),
331+
buildEvent({ id: 'b', created_at: 20 }),
332+
buildEvent({ id: 'd', created_at: 10 }),
333+
buildEvent({ id: 'e', created_at: 10 }),
334+
buildEvent({ id: 'd', created_at: 10 }),
335+
]
336+
console.log('==================')
337+
const result = mergeReverseSortedLists(list1, list2)
338+
console.log(
339+
'result:',
340+
result.map(e => e.id),
341+
)
342+
expect(result.map(e => e.id)).toEqual(['a', 'c', 'b', 'd', 'e'])
343+
})
344+
345+
test('merge when one list is completely before the other', () => {
346+
const list1 = [buildEvent({ id: 'a', created_at: 50 }), buildEvent({ id: 'b', created_at: 40 })]
347+
const list2 = [buildEvent({ id: 'c', created_at: 30 }), buildEvent({ id: 'd', created_at: 20 })]
348+
const result = mergeReverseSortedLists(list1, list2)
349+
expect(result).toHaveLength(4)
350+
expect(result.map(e => e.id)).toEqual(['a', 'b', 'c', 'd'])
351+
})
352+
353+
test('merge when one list is completely after the other', () => {
354+
const list1 = [buildEvent({ id: 'a', created_at: 10 }), buildEvent({ id: 'b', created_at: 5 })]
355+
const list2 = [buildEvent({ id: 'c', created_at: 30 }), buildEvent({ id: 'd', created_at: 20 })]
356+
const result = mergeReverseSortedLists(list1, list2)
357+
expect(result).toHaveLength(4)
358+
expect(result.map(e => e.id)).toEqual(['c', 'd', 'a', 'b'])
359+
})
360+
})
361+
273362
describe('normalizeURL', () => {
274363
test('normalizes wss:// URLs', () => {
275364
expect(normalizeURL('wss://example.com')).toBe('wss://example.com/')

utils.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Event } from './core.ts'
1+
import type { NostrEvent } from './core.ts'
22

33
export const utf8Decoder: TextDecoder = new TextDecoder('utf-8')
44
export const utf8Encoder: TextEncoder = new TextEncoder()
@@ -22,7 +22,7 @@ export function normalizeURL(url: string): string {
2222
}
2323
}
2424

25-
export function insertEventIntoDescendingList(sortedArray: Event[], event: Event): Event[] {
25+
export function insertEventIntoDescendingList(sortedArray: NostrEvent[], event: NostrEvent): NostrEvent[] {
2626
const [idx, found] = binarySearch(sortedArray, b => {
2727
if (event.id === b.id) return 0
2828
if (event.created_at === b.created_at) return -1
@@ -34,7 +34,7 @@ export function insertEventIntoDescendingList(sortedArray: Event[], event: Event
3434
return sortedArray
3535
}
3636

37-
export function insertEventIntoAscendingList(sortedArray: Event[], event: Event): Event[] {
37+
export function insertEventIntoAscendingList(sortedArray: NostrEvent[], event: NostrEvent): NostrEvent[] {
3838
const [idx, found] = binarySearch(sortedArray, b => {
3939
if (event.id === b.id) return 0
4040
if (event.created_at === b.created_at) return -1
@@ -68,6 +68,62 @@ export function binarySearch<T>(arr: T[], compare: (b: T) => number): [number, b
6868
return [start, false]
6969
}
7070

71+
export function mergeReverseSortedLists(list1: NostrEvent[], list2: NostrEvent[]): NostrEvent[] {
72+
const result: NostrEvent[] = new Array(list1.length + list2.length)
73+
result.length = 0
74+
let i1 = 0
75+
let i2 = 0
76+
let sameTimestampIds: string[] = []
77+
78+
while (i1 < list1.length && i2 < list2.length) {
79+
let next: NostrEvent
80+
if (list1[i1]?.created_at > list2[i2]?.created_at) {
81+
next = list1[i1]
82+
i1++
83+
} else {
84+
next = list2[i2]
85+
i2++
86+
}
87+
88+
if (result.length > 0 && result[result.length - 1].created_at === next.created_at) {
89+
if (sameTimestampIds.includes(next.id)) continue
90+
} else {
91+
sameTimestampIds.length = 0
92+
}
93+
94+
result.push(next)
95+
sameTimestampIds.push(next.id)
96+
}
97+
98+
while (i1 < list1.length) {
99+
const next = list1[i1]
100+
i1++
101+
102+
if (result.length > 0 && result[result.length - 1].created_at === next.created_at) {
103+
if (sameTimestampIds.includes(next.id)) continue
104+
} else {
105+
sameTimestampIds.length = 0
106+
}
107+
result.push(next)
108+
sameTimestampIds.push(next.id)
109+
}
110+
111+
while (i2 < list2.length) {
112+
const next = list2[i2]
113+
i2++
114+
115+
if (result.length > 0 && result[result.length - 1].created_at === next.created_at) {
116+
if (sameTimestampIds.includes(next.id)) continue
117+
} else {
118+
sameTimestampIds.length = 0
119+
}
120+
result.push(next)
121+
sameTimestampIds.push(next.id)
122+
}
123+
124+
return result
125+
}
126+
71127
export class QueueNode<V> {
72128
public value: V
73129
public next: QueueNode<V> | null = null

0 commit comments

Comments
 (0)