diff --git a/index.ts b/index.ts index eda6ea3..29f075f 100644 --- a/index.ts +++ b/index.ts @@ -3,14 +3,15 @@ export * from './relay' export * from './event' export * from './filter' -export * as fj from './fakejson' - export * as nip04 from './nip04' export * as nip05 from './nip05' export * as nip06 from './nip06' export * as nip19 from './nip19' export * as nip26 from './nip26' +export * as fj from './fakejson' +export * as utils from './utils' + // monkey patch secp256k1 import * as secp256k1 from '@noble/secp256k1' import {hmac} from '@noble/hashes/hmac' diff --git a/package.json b/package.json index 76bf071..1669c9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nostr-tools", - "version": "1.2.0", + "version": "1.2.1", "description": "Tools for making a Nostr client.", "repository": { "type": "git", diff --git a/utils.test.js b/utils.test.js new file mode 100644 index 0000000..a86aba7 --- /dev/null +++ b/utils.test.js @@ -0,0 +1,183 @@ +/* eslint-env jest */ + +const {utils} = require('./lib/nostr.cjs') + +const {insertEventIntoAscendingList, insertEventIntoDescendingList} = utils + +describe('inserting into a desc sorted list of events', () => { + test('insert into an empty list', async () => { + const list0 = [] + expect( + insertEventIntoDescendingList(list0, {id: 'abc', created_at: 10}) + ).toHaveLength(1) + }) + + test('insert in the beginning of a list', async () => { + const list0 = [{created_at: 20}, {created_at: 10}] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [{created_at: 30}, {created_at: 20}, {created_at: 10}] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [ + {created_at: 30}, + {created_at: 20}, + {created_at: 10}, + {created_at: 1} + ] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 10} + ] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 10} + ] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 10, id: 'abc'} + ] + const list1 = insertEventIntoDescendingList(list0, { + 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 = [] + expect( + insertEventIntoAscendingList(list0, {id: 'abc', created_at: 10}) + ).toHaveLength(1) + }) + + test('insert in the beginning of a list', async () => { + const list0 = [{created_at: 10}, {created_at: 20}] + const list1 = insertEventIntoAscendingList(list0, { + 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 = [{created_at: 10}, {created_at: 20}, {created_at: 30}] + const list1 = insertEventIntoAscendingList(list0, { + 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 = [ + {created_at: 10}, + {created_at: 20}, + {created_at: 30}, + {created_at: 40} + ] + const list1 = insertEventIntoAscendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 40} + ] + const list1 = insertEventIntoAscendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 20}, + {created_at: 30} + ] + const list1 = insertEventIntoAscendingList(list0, { + 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 = [ + {created_at: 20}, + {created_at: 20}, + {created_at: 30, id: 'abc'} + ] + const list1 = insertEventIntoAscendingList(list0, { + id: 'abc', + created_at: 30 + }) + expect(list1).toHaveLength(3) + }) +}) diff --git a/utils.ts b/utils.ts index 784a868..787afd5 100644 --- a/utils.ts +++ b/utils.ts @@ -1,2 +1,97 @@ +import {Event} from './event' + export const utf8Decoder = new TextDecoder('utf-8') export const utf8Encoder = new TextEncoder() + +// +// fast insert-into-sorted-array functions adapted from https://github.com/terrymorse58/fast-sorted-array +// +export function insertEventIntoDescendingList( + sortedArray: Event[], + event: Event +) { + let start = 0 + let end = sortedArray.length - 1 + let midPoint + let position = start + + if (end < 0) { + position = 0 + } else if (event.created_at < sortedArray[end].created_at) { + position = end + 1 + } else if (event.created_at >= sortedArray[start].created_at) { + position = start + } else + while (true) { + if (end <= start + 1) { + position = end + break + } + midPoint = Math.floor(start + (end - start) / 2) + if (sortedArray[midPoint].created_at > event.created_at) { + start = midPoint + } else if (sortedArray[midPoint].created_at < event.created_at) { + end = midPoint + } else { + // aMidPoint === num + position = midPoint + break + } + } + + // insert when num is NOT already in (no duplicates) + if (sortedArray[position]?.id !== event.id) { + return [ + ...sortedArray.slice(0, position), + event, + ...sortedArray.slice(position) + ] + } + + return sortedArray +} + +export function insertEventIntoAscendingList( + sortedArray: Event[], + event: Event +) { + let start = 0 + let end = sortedArray.length - 1 + let midPoint + let position = start + + if (end < 0) { + position = 0 + } else if (event.created_at > sortedArray[end].created_at) { + position = end + 1 + } else if (event.created_at <= sortedArray[start].created_at) { + position = start + } else + while (true) { + if (end <= start + 1) { + position = end + break + } + midPoint = Math.floor(start + (end - start) / 2) + if (sortedArray[midPoint].created_at < event.created_at) { + start = midPoint + } else if (sortedArray[midPoint].created_at > event.created_at) { + end = midPoint + } else { + // aMidPoint === num + position = midPoint + break + } + } + + // insert when num is NOT already in (no duplicates) + if (sortedArray[position]?.id !== event.id) { + return [ + ...sortedArray.slice(0, position), + event, + ...sortedArray.slice(position) + ] + } + + return sortedArray +}