mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2026-02-01 14:55:51 +00:00
401 lines
13 KiB
TypeScript
401 lines
13 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
import { buildEvent } from './test-helpers.ts'
|
|
import {
|
|
Queue,
|
|
insertEventIntoAscendingList,
|
|
insertEventIntoDescendingList,
|
|
binarySearch,
|
|
normalizeURL,
|
|
mergeReverseSortedLists,
|
|
} from './utils.ts'
|
|
|
|
import type { Event } from './core.ts'
|
|
|
|
describe('inserting into a desc sorted list of events', () => {
|
|
test('insert into an empty list', async () => {
|
|
const list0: Event[] = []
|
|
expect(insertEventIntoDescendingList(list0, buildEvent({ id: 'abc', created_at: 10 }))).toHaveLength(1)
|
|
})
|
|
|
|
test('insert in the beginning of a list', async () => {
|
|
const list0 = [buildEvent({ created_at: 20 }), buildEvent({ created_at: 10 })]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 30,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(3)
|
|
expect(list1[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the beginning of a list with same created_at', async () => {
|
|
const list0 = [buildEvent({ created_at: 30 }), buildEvent({ created_at: 20 }), buildEvent({ created_at: 10 })]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 30,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(4)
|
|
expect(list1[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the middle of a list', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 30 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 10 }),
|
|
buildEvent({ created_at: 1 }),
|
|
]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 15,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(5)
|
|
expect(list1[2].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the end of a list', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 10 }),
|
|
]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 5,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(6)
|
|
expect(list1.slice(-1)[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the last-to-end of a list with same created_at', async () => {
|
|
const list0: Event[] = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 10 }),
|
|
]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 10,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(6)
|
|
expect(list1.slice(-2)[0].id).toBe('abc')
|
|
})
|
|
|
|
test('do not insert duplicates', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 10, id: 'abc' }),
|
|
]
|
|
const list1 = insertEventIntoDescendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 10,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
describe('inserting into a asc sorted list of events', () => {
|
|
test('insert into an empty list', async () => {
|
|
const list0: Event[] = []
|
|
expect(insertEventIntoAscendingList(list0, buildEvent({ id: 'abc', created_at: 10 }))).toHaveLength(1)
|
|
})
|
|
|
|
test('insert in the beginning of a list', async () => {
|
|
const list0 = [buildEvent({ created_at: 10 }), buildEvent({ created_at: 20 })]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 1,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(3)
|
|
expect(list1[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the beginning of a list with same created_at', async () => {
|
|
const list0 = [buildEvent({ created_at: 10 }), buildEvent({ created_at: 20 }), buildEvent({ created_at: 30 })]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 10,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(4)
|
|
expect(list1[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the middle of a list', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 10 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 30 }),
|
|
buildEvent({ created_at: 40 }),
|
|
]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 25,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(5)
|
|
expect(list1[2].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the end of a list', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 40 }),
|
|
]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 50,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(6)
|
|
expect(list1.slice(-1)[0].id).toBe('abc')
|
|
})
|
|
|
|
test('insert in the last-to-end of a list with same created_at', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 30 }),
|
|
]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 30,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(6)
|
|
expect(list1.slice(-2)[0].id).toBe('abc')
|
|
})
|
|
|
|
test('do not insert duplicates', async () => {
|
|
const list0 = [
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 20 }),
|
|
buildEvent({ created_at: 30, id: 'abc' }),
|
|
]
|
|
const list1 = insertEventIntoAscendingList(
|
|
list0,
|
|
buildEvent({
|
|
id: 'abc',
|
|
created_at: 30,
|
|
}),
|
|
)
|
|
expect(list1).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
describe('enqueue a message into MessageQueue', () => {
|
|
test('enqueue into an empty queue', () => {
|
|
const queue = new Queue()
|
|
queue.enqueue('node1')
|
|
expect(queue.first!.value).toBe('node1')
|
|
})
|
|
test('enqueue into a non-empty queue', () => {
|
|
const queue = new Queue()
|
|
queue.enqueue('node1')
|
|
queue.enqueue('node3')
|
|
queue.enqueue('node2')
|
|
expect(queue.first!.value).toBe('node1')
|
|
expect(queue.last!.value).toBe('node2')
|
|
})
|
|
test('dequeue from an empty queue', () => {
|
|
const queue = new Queue()
|
|
const item1 = queue.dequeue()
|
|
expect(item1).toBe(null)
|
|
})
|
|
test('dequeue from a non-empty queue', () => {
|
|
const queue = new Queue()
|
|
queue.enqueue('node1')
|
|
queue.enqueue('node3')
|
|
queue.enqueue('node2')
|
|
const item1 = queue.dequeue()
|
|
expect(item1).toBe('node1')
|
|
const item2 = queue.dequeue()
|
|
expect(item2).toBe('node3')
|
|
})
|
|
test('dequeue more than in queue', () => {
|
|
const queue = new Queue()
|
|
queue.enqueue('node1')
|
|
queue.enqueue('node3')
|
|
const item1 = queue.dequeue()
|
|
expect(item1).toBe('node1')
|
|
const item2 = queue.dequeue()
|
|
expect(item2).toBe('node3')
|
|
const item3 = queue.dequeue()
|
|
expect(item3).toBe(null)
|
|
})
|
|
})
|
|
|
|
test('binary search', () => {
|
|
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('e' < b ? -1 : 'e' === b ? 0 : 1))).toEqual([3, true])
|
|
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('x' < b ? -1 : 'x' === b ? 0 : 1))).toEqual([4, false])
|
|
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('c' < b ? -1 : 'c' === b ? 0 : 1))).toEqual([2, false])
|
|
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('a' < b ? -1 : 'a' === b ? 0 : 1))).toEqual([0, true])
|
|
expect(binarySearch(['a', 'b', 'd', 'e'], b => ('[' < b ? -1 : '[' === b ? 0 : 1))).toEqual([0, false])
|
|
})
|
|
|
|
describe('mergeReverseSortedLists', () => {
|
|
test('merge empty lists', () => {
|
|
const list1: Event[] = []
|
|
const list2: Event[] = []
|
|
expect(mergeReverseSortedLists(list1, list2)).toHaveLength(0)
|
|
})
|
|
|
|
test('merge list with empty list', () => {
|
|
const list1 = [buildEvent({ id: 'a', created_at: 30 }), buildEvent({ id: 'b', created_at: 20 })]
|
|
const list2: Event[] = []
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
expect(result).toHaveLength(2)
|
|
expect(result.map(e => e.id)).toEqual(['a', 'b'])
|
|
})
|
|
|
|
test('merge two simple lists', () => {
|
|
const list1 = [
|
|
buildEvent({ id: 'a', created_at: 30 }),
|
|
buildEvent({ id: 'b', created_at: 10 }),
|
|
buildEvent({ id: 'f', created_at: 3 }),
|
|
buildEvent({ id: 'g', created_at: 2 }),
|
|
]
|
|
const list2 = [
|
|
buildEvent({ id: 'c', created_at: 25 }),
|
|
buildEvent({ id: 'd', created_at: 5 }),
|
|
buildEvent({ id: 'e', created_at: 1 }),
|
|
]
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
expect(result.map(e => e.id)).toEqual(['a', 'c', 'b', 'd', 'f', 'g', 'e'])
|
|
})
|
|
|
|
test('merge lists with same timestamps', () => {
|
|
const list1 = [
|
|
buildEvent({ id: 'a', created_at: 30 }),
|
|
buildEvent({ id: 'b', created_at: 20 }),
|
|
buildEvent({ id: 'f', created_at: 10 }),
|
|
]
|
|
const list2 = [
|
|
buildEvent({ id: 'c', created_at: 30 }),
|
|
buildEvent({ id: 'd', created_at: 20 }),
|
|
buildEvent({ id: 'e', created_at: 20 }),
|
|
]
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
expect(result.map(e => e.id)).toEqual(['c', 'a', 'd', 'e', 'b', 'f'])
|
|
})
|
|
|
|
test('deduplicate events with same timestamp and id', () => {
|
|
const list1 = [
|
|
buildEvent({ id: 'a', created_at: 30 }),
|
|
buildEvent({ id: 'b', created_at: 20 }),
|
|
buildEvent({ id: 'b', created_at: 20 }),
|
|
buildEvent({ id: 'c', created_at: 20 }),
|
|
buildEvent({ id: 'd', created_at: 10 }),
|
|
]
|
|
const list2 = [
|
|
buildEvent({ id: 'a', created_at: 30 }),
|
|
buildEvent({ id: 'c', created_at: 20 }),
|
|
buildEvent({ id: 'b', created_at: 20 }),
|
|
buildEvent({ id: 'd', created_at: 10 }),
|
|
buildEvent({ id: 'e', created_at: 10 }),
|
|
buildEvent({ id: 'd', created_at: 10 }),
|
|
]
|
|
console.log('==================')
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
console.log(
|
|
'result:',
|
|
result.map(e => e.id),
|
|
)
|
|
expect(result.map(e => e.id)).toEqual(['a', 'c', 'b', 'd', 'e'])
|
|
})
|
|
|
|
test('merge when one list is completely before the other', () => {
|
|
const list1 = [buildEvent({ id: 'a', created_at: 50 }), buildEvent({ id: 'b', created_at: 40 })]
|
|
const list2 = [buildEvent({ id: 'c', created_at: 30 }), buildEvent({ id: 'd', created_at: 20 })]
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
expect(result).toHaveLength(4)
|
|
expect(result.map(e => e.id)).toEqual(['a', 'b', 'c', 'd'])
|
|
})
|
|
|
|
test('merge when one list is completely after the other', () => {
|
|
const list1 = [buildEvent({ id: 'a', created_at: 10 }), buildEvent({ id: 'b', created_at: 5 })]
|
|
const list2 = [buildEvent({ id: 'c', created_at: 30 }), buildEvent({ id: 'd', created_at: 20 })]
|
|
const result = mergeReverseSortedLists(list1, list2)
|
|
expect(result).toHaveLength(4)
|
|
expect(result.map(e => e.id)).toEqual(['c', 'd', 'a', 'b'])
|
|
})
|
|
})
|
|
|
|
describe('normalizeURL', () => {
|
|
test('normalizes wss:// URLs', () => {
|
|
expect(normalizeURL('wss://example.com')).toBe('wss://example.com/')
|
|
expect(normalizeURL('wss://example.com/')).toBe('wss://example.com/')
|
|
expect(normalizeURL('wss://example.com//path')).toBe('wss://example.com/path')
|
|
expect(normalizeURL('wss://example.com:443')).toBe('wss://example.com/')
|
|
})
|
|
|
|
test('normalizes https:// URLs', () => {
|
|
expect(normalizeURL('https://example.com')).toBe('wss://example.com/')
|
|
expect(normalizeURL('https://example.com/')).toBe('wss://example.com/')
|
|
expect(normalizeURL('http://example.com//path')).toBe('ws://example.com/path')
|
|
})
|
|
|
|
test('normalizes ws:// URLs', () => {
|
|
expect(normalizeURL('ws://example.com')).toBe('ws://example.com/')
|
|
expect(normalizeURL('ws://example.com/')).toBe('ws://example.com/')
|
|
expect(normalizeURL('ws://example.com//path')).toBe('ws://example.com/path')
|
|
expect(normalizeURL('ws://example.com:80')).toBe('ws://example.com/')
|
|
})
|
|
|
|
test('adds wss:// to URLs without scheme', () => {
|
|
expect(normalizeURL('example.com')).toBe('wss://example.com/')
|
|
expect(normalizeURL('example.com/')).toBe('wss://example.com/')
|
|
expect(normalizeURL('example.com//path')).toBe('wss://example.com/path')
|
|
})
|
|
|
|
test('handles query parameters', () => {
|
|
expect(normalizeURL('wss://example.com?z=1&a=2')).toBe('wss://example.com/?a=2&z=1')
|
|
})
|
|
|
|
test('removes hash', () => {
|
|
expect(normalizeURL('wss://example.com#hash')).toBe('wss://example.com/')
|
|
})
|
|
|
|
test('throws on invalid URL', () => {
|
|
expect(() => normalizeURL('http://')).toThrow('Invalid URL: http://')
|
|
})
|
|
})
|