Compare commits

...

10 Commits

Author SHA1 Message Date
umk0m1qk
354b80a929 fix(relay): move _connected = false above closeAllSubscriptions() in close()
close() was setting _connected = false after closeAllSubscriptions(), which meant each sub still saw the relay as connected and tried to send CLOSE frames. Those sends get queued as microtasks, but by the time they run the socket is already closing, so you get a bunch of "WebSocket is already in CLOSING or CLOSED state" warnings.

handleHardClose() already gets this order right — this just makes close() consistent with that.
2026-02-11 12:24:58 -03:00
fiatjaf
0c2c2cd4d8 nip13: improve mining by skipping hex. 2026-02-08 01:06:01 -03:00
fiatjaf
28f7553187 fix a type so jsr is happy. 2026-02-02 18:49:17 -03:00
fiatjaf
ca29d9b515 ok, we need the prepareSubscription method. 2026-02-02 18:46:50 -03:00
fiatjaf
ab802c8dbe automatic prune broken relay objects and keep track of relay idleness so they can be pruned. 2026-02-02 18:44:52 -03:00
fiatjaf
9db705d86c delete queue test since we don't have queues anymore. 2026-02-02 17:01:59 -03:00
fiatjaf
be9b91318f relay: get rid of the message queue, because js is single-threaded. 2026-02-02 09:06:56 -03:00
fiatjaf
c2423f7f31 nip27: fix hashtag parsing after newline or other characters. 2026-02-02 08:56:18 -03:00
fiatjaf
05b1fba511 export source files so they can be imported by other ts apps and libraries better. 2026-02-02 00:38:15 -03:00
fiatjaf
2d1345096b subscribeMany and subscribeManyEose are the same as subscribe/subscribeEose. 2026-02-01 17:19:58 -03:00
10 changed files with 236 additions and 300 deletions

View File

@@ -88,9 +88,7 @@ export class AbstractSimplePool {
enableReconnect: this.enableReconnect, enableReconnect: this.enableReconnect,
}) })
relay.onclose = () => { relay.onclose = () => {
if (relay && !relay.enableReconnect) { this.relays.delete(url)
this.relays.delete(url)
}
} }
this.relays.set(url, relay) this.relays.set(url, relay)
} }
@@ -102,10 +100,15 @@ export class AbstractSimplePool {
} }
} }
await relay.connect({ try {
timeout: params?.connectionTimeout, await relay.connect({
abort: params?.abort, timeout: params?.connectionTimeout,
}) abort: params?.abort,
})
} catch (err) {
this.relays.delete(url)
throw err
}
return relay return relay
} }
@@ -119,10 +122,14 @@ export class AbstractSimplePool {
subscribe(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser { subscribe(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser {
const request: { url: string; filter: Filter }[] = [] const request: { url: string; filter: Filter }[] = []
const uniqUrls: string[] = []
for (let i = 0; i < relays.length; i++) { for (let i = 0; i < relays.length; i++) {
const url = normalizeURL(relays[i]) const url = normalizeURL(relays[i])
if (!request.find(r => r.url === url)) { if (!request.find(r => r.url === url)) {
request.push({ url, filter: filter }) if (uniqUrls.indexOf(url) === -1) {
uniqUrls.push(url)
request.push({ url, filter: filter })
}
} }
} }
@@ -130,17 +137,7 @@ export class AbstractSimplePool {
} }
subscribeMany(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser { subscribeMany(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser {
const request: { url: string; filter: Filter }[] = [] return this.subscribe(relays, filter, params)
const uniqUrls: string[] = []
for (let i = 0; i < relays.length; i++) {
const url = normalizeURL(relays[i])
if (uniqUrls.indexOf(url) === -1) {
uniqUrls.push(url)
request.push({ url, filter: filter })
}
}
return this.subscribeMap(request, params)
} }
subscribeMap(requests: { url: string; filter: Filter }[], params: SubscribeManyParams): SubCloser { subscribeMap(requests: { url: string; filter: Filter }[], params: SubscribeManyParams): SubCloser {
@@ -286,13 +283,7 @@ export class AbstractSimplePool {
filter: Filter, filter: Filter,
params: Pick<SubscribeManyParams, 'label' | 'id' | 'onevent' | 'onclose' | 'maxWait' | 'onauth'>, params: Pick<SubscribeManyParams, 'label' | 'id' | 'onevent' | 'onclose' | 'maxWait' | 'onauth'>,
): SubCloser { ): SubCloser {
const subcloser = this.subscribeMany(relays, filter, { return this.subscribeEose(relays, filter, params)
...params,
oneose() {
subcloser.close('closed automatically on eose')
},
})
return subcloser
} }
async querySync( async querySync(
@@ -392,4 +383,19 @@ export class AbstractSimplePool {
this.relays.forEach(conn => conn.close()) this.relays.forEach(conn => conn.close())
this.relays = new Map() this.relays = new Map()
} }
pruneIdleRelays(idleThresholdMs: number = 10000): string[] {
const prunedUrls: string[] = []
// check each relay's idle status and prune if over threshold
for (const [url, relay] of this.relays) {
if (relay.idleSince && Date.now() - relay.idleSince >= idleThresholdMs) {
this.relays.delete(url)
prunedUrls.push(url)
relay.close()
}
}
return prunedUrls
}
} }

View File

@@ -3,9 +3,8 @@
import type { Event, EventTemplate, VerifiedEvent, Nostr, NostrEvent } from './core.ts' import type { Event, EventTemplate, VerifiedEvent, Nostr, NostrEvent } from './core.ts'
import { matchFilters, type Filter } from './filter.ts' import { matchFilters, type Filter } from './filter.ts'
import { getHex64, getSubscriptionId } from './fakejson.ts' import { getHex64, getSubscriptionId } from './fakejson.ts'
import { Queue, normalizeURL } from './utils.ts' import { normalizeURL } from './utils.ts'
import { makeAuthEvent } from './nip42.ts' import { makeAuthEvent } from './nip42.ts'
import { yieldThread } from './helpers.ts'
type RelayWebSocket = WebSocket & { type RelayWebSocket = WebSocket & {
ping?(): void ping?(): void
@@ -42,6 +41,8 @@ export class AbstractRelay {
public openSubs: Map<string, Subscription> = new Map() public openSubs: Map<string, Subscription> = new Map()
public enablePing: boolean | undefined public enablePing: boolean | undefined
public enableReconnect: boolean public enableReconnect: boolean
public idleSince: number | undefined = Date.now() // when undefined that means it isn't idle
public ongoingOperations: number = 0 // used to compute idleness
private reconnectTimeoutHandle: ReturnType<typeof setTimeout> | undefined private reconnectTimeoutHandle: ReturnType<typeof setTimeout> | undefined
private pingIntervalHandle: ReturnType<typeof setInterval> | undefined private pingIntervalHandle: ReturnType<typeof setInterval> | undefined
private reconnectAttempts: number = 0 private reconnectAttempts: number = 0
@@ -51,8 +52,6 @@ export class AbstractRelay {
private openCountRequests = new Map<string, CountResolver>() private openCountRequests = new Map<string, CountResolver>()
private openEventPublishes = new Map<string, EventPublishResolver>() private openEventPublishes = new Map<string, EventPublishResolver>()
private ws: RelayWebSocket | undefined private ws: RelayWebSocket | undefined
private incomingMessageQueue = new Queue<string>()
private queueRunning = false
private challenge: string | undefined private challenge: string | undefined
private authPromise: Promise<string> | undefined private authPromise: Promise<string> | undefined
private serial: number = 0 private serial: number = 0
@@ -119,12 +118,12 @@ export class AbstractRelay {
this._connected = false this._connected = false
this.connectionPromise = undefined this.connectionPromise = undefined
this.idleSince = undefined
this.onclose?.()
if (this.enableReconnect && !this.skipReconnection) { if (this.enableReconnect && !this.skipReconnection) {
this.reconnect() this.reconnect()
} else { } else {
this.onclose?.()
this.closeAllSubscriptions(reason) this.closeAllSubscriptions(reason)
} }
} }
@@ -230,7 +229,7 @@ export class AbstractRelay {
const sub = this.subscribe( const sub = this.subscribe(
[{ ids: ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], limit: 0 }], [{ ids: ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], limit: 0 }],
{ {
label: 'forced-ping', label: '<forced-ping>',
oneose: () => { oneose: () => {
resolve(true) resolve(true)
sub.close() sub.close()
@@ -269,21 +268,126 @@ export class AbstractRelay {
} }
} }
private async runQueue() { public async send(message: string) {
this.queueRunning = true if (!this.connectionPromise) throw new SendingOnClosedConnection(message, this.url)
while (true) {
if (false === this.handleNext()) { this.connectionPromise.then(() => {
break this.ws?.send(message)
} })
await yieldThread()
}
this.queueRunning = false
} }
private handleNext(): undefined | false { public async auth(signAuthEvent: (evt: EventTemplate) => Promise<VerifiedEvent>): Promise<string> {
const json = this.incomingMessageQueue.dequeue() const challenge = this.challenge
if (!challenge) throw new Error("can't perform auth, no challenge was received")
if (this.authPromise) return this.authPromise
this.authPromise = new Promise<string>(async (resolve, reject) => {
try {
let evt = await signAuthEvent(makeAuthEvent(this.url, challenge))
let timeout = setTimeout(() => {
let ep = this.openEventPublishes.get(evt.id) as EventPublishResolver
if (ep) {
ep.reject(new Error('auth timed out'))
this.openEventPublishes.delete(evt.id)
}
}, this.publishTimeout)
this.openEventPublishes.set(evt.id, { resolve, reject, timeout })
this.send('["AUTH",' + JSON.stringify(evt) + ']')
} catch (err) {
console.warn('subscribe auth function failed:', err)
}
})
return this.authPromise
}
public async publish(event: Event): Promise<string> {
this.idleSince = undefined
this.ongoingOperations++
const ret = new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
const ep = this.openEventPublishes.get(event.id) as EventPublishResolver
if (ep) {
ep.reject(new Error('publish timed out'))
this.openEventPublishes.delete(event.id)
}
}, this.publishTimeout)
this.openEventPublishes.set(event.id, { resolve, reject, timeout })
})
this.send('["EVENT",' + JSON.stringify(event) + ']')
// compute idleness state
this.ongoingOperations--
if (this.ongoingOperations === 0) this.idleSince = Date.now()
return ret
}
public async count(filters: Filter[], params: { id?: string | null }): Promise<number> {
this.serial++
const id = params?.id || 'count:' + this.serial
const ret = new Promise<number>((resolve, reject) => {
this.openCountRequests.set(id, { resolve, reject })
})
this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1))
return ret
}
public subscribe(
filters: Filter[],
params: Partial<SubscriptionParams> & { label?: string; id?: string },
): Subscription {
if (params.label !== '<forced-ping>') {
this.idleSince = undefined
this.ongoingOperations++
}
const sub = this.prepareSubscription(filters, params)
sub.fire()
if (params.abort) {
params.abort.onabort = () => sub.close(String(params.abort!.reason || '<aborted>'))
}
return sub
}
public prepareSubscription(
filters: Filter[],
params: Partial<SubscriptionParams> & { label?: string; id?: string },
): Subscription {
this.serial++
const id = params.id || (params.label ? params.label + ':' : 'sub:') + this.serial
const sub = new Subscription(this, id, filters, params)
this.openSubs.set(id, sub)
return sub
}
public close() {
this.skipReconnection = true
if (this.reconnectTimeoutHandle) {
clearTimeout(this.reconnectTimeoutHandle)
this.reconnectTimeoutHandle = undefined
}
if (this.pingIntervalHandle) {
clearInterval(this.pingIntervalHandle)
this.pingIntervalHandle = undefined
}
this._connected = false
this.closeAllSubscriptions('relay connection closed by us')
this.idleSince = undefined
this.onclose?.()
if (this.ws?.readyState === this._WebSocket.OPEN) {
this.ws?.close()
}
}
// this is the function assigned to this.ws.onmessage
// it's exposed for testing and debugging purposes
public _onmessage(ev: MessageEvent<any>): void {
const json = ev.data
if (!json) { if (!json) {
return false return
} }
// shortcut EVENT sub // shortcut EVENT sub
@@ -381,118 +485,11 @@ export class AbstractRelay {
} }
} }
} catch (err) { } catch (err) {
const [_, __, event] = JSON.parse(json)
;(window as any).printer.maybe(event.pubkey, ':: caught err', event, this.url, err)
return return
} }
} }
public async send(message: string) {
if (!this.connectionPromise) throw new SendingOnClosedConnection(message, this.url)
this.connectionPromise.then(() => {
this.ws?.send(message)
})
}
public async auth(signAuthEvent: (evt: EventTemplate) => Promise<VerifiedEvent>): Promise<string> {
const challenge = this.challenge
if (!challenge) throw new Error("can't perform auth, no challenge was received")
if (this.authPromise) return this.authPromise
this.authPromise = new Promise<string>(async (resolve, reject) => {
try {
let evt = await signAuthEvent(makeAuthEvent(this.url, challenge))
let timeout = setTimeout(() => {
let ep = this.openEventPublishes.get(evt.id) as EventPublishResolver
if (ep) {
ep.reject(new Error('auth timed out'))
this.openEventPublishes.delete(evt.id)
}
}, this.publishTimeout)
this.openEventPublishes.set(evt.id, { resolve, reject, timeout })
this.send('["AUTH",' + JSON.stringify(evt) + ']')
} catch (err) {
console.warn('subscribe auth function failed:', err)
}
})
return this.authPromise
}
public async publish(event: Event): Promise<string> {
const ret = new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
const ep = this.openEventPublishes.get(event.id) as EventPublishResolver
if (ep) {
ep.reject(new Error('publish timed out'))
this.openEventPublishes.delete(event.id)
}
}, this.publishTimeout)
this.openEventPublishes.set(event.id, { resolve, reject, timeout })
})
this.send('["EVENT",' + JSON.stringify(event) + ']')
return ret
}
public async count(filters: Filter[], params: { id?: string | null }): Promise<number> {
this.serial++
const id = params?.id || 'count:' + this.serial
const ret = new Promise<number>((resolve, reject) => {
this.openCountRequests.set(id, { resolve, reject })
})
this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1))
return ret
}
public subscribe(
filters: Filter[],
params: Partial<SubscriptionParams> & { label?: string; id?: string },
): Subscription {
const sub = this.prepareSubscription(filters, params)
sub.fire()
if (params.abort) {
params.abort.onabort = () => sub.close(String(params.abort!.reason || '<aborted>'))
}
return sub
}
public prepareSubscription(
filters: Filter[],
params: Partial<SubscriptionParams> & { label?: string; id?: string },
): Subscription {
this.serial++
const id = params.id || (params.label ? params.label + ':' : 'sub:') + this.serial
const subscription = new Subscription(this, id, filters, params)
this.openSubs.set(id, subscription)
return subscription
}
public close() {
this.skipReconnection = true
if (this.reconnectTimeoutHandle) {
clearTimeout(this.reconnectTimeoutHandle)
this.reconnectTimeoutHandle = undefined
}
if (this.pingIntervalHandle) {
clearInterval(this.pingIntervalHandle)
this.pingIntervalHandle = undefined
}
this.closeAllSubscriptions('relay connection closed by us')
this._connected = false
this.onclose?.()
if (this.ws?.readyState === this._WebSocket.OPEN) {
this.ws?.close()
}
}
// this is the function assigned to this.ws.onmessage
// it's exposed for testing and debugging purposes
public _onmessage(ev: MessageEvent<any>) {
this.incomingMessageQueue.enqueue(ev.data as string)
if (!this.queueRunning) {
this.runQueue()
}
}
} }
export class Subscription { export class Subscription {
@@ -568,6 +565,11 @@ export class Subscription {
this.closed = true this.closed = true
} }
this.relay.openSubs.delete(this.id) this.relay.openSubs.delete(this.id)
// compute idleness state
this.relay.ongoingOperations--
if (this.relay.ongoingOperations === 0) this.relay.idleSince = Date.now()
this.onclose?.(reason) this.onclose?.(reason)
} }
} }

View File

@@ -1,37 +1,5 @@
import { verifiedSymbol, type Event, type Nostr, VerifiedEvent } from './core.ts' import { verifiedSymbol, type Event, type Nostr, VerifiedEvent } from './core.ts'
export async function yieldThread() {
return new Promise<void>((resolve, reject) => {
try {
// Check if MessageChannel is available
if (typeof MessageChannel !== 'undefined') {
const ch = new MessageChannel()
const handler = () => {
// @ts-ignore (typescript thinks this property should be called `removeListener`, but in fact it's `removeEventListener`)
ch.port1.removeEventListener('message', handler)
resolve()
}
// @ts-ignore (typescript thinks this property should be called `addListener`, but in fact it's `addEventListener`)
ch.port1.addEventListener('message', handler)
ch.port2.postMessage(0)
ch.port1.start()
} else {
if (typeof setImmediate !== 'undefined') {
setImmediate(resolve)
} else if (typeof setTimeout !== 'undefined') {
setTimeout(resolve, 0)
} else {
// Last resort - resolve immediately
resolve()
}
}
} catch (e) {
console.error('during yield: ', e)
reject(e)
}
})
}
export const alwaysTrue: Nostr['verifyEvent'] = (t: Event): t is VerifiedEvent => { export const alwaysTrue: Nostr['verifyEvent'] = (t: Event): t is VerifiedEvent => {
t[verifiedSymbol] = true t[verifiedSymbol] = true
return true return true

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nostr/tools", "name": "@nostr/tools",
"version": "2.22.2", "version": "2.23.0",
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./core": "./core.ts", "./core": "./core.ts",

View File

@@ -21,6 +21,23 @@ export function getPow(hex: string): number {
return count return count
} }
/** Get POW difficulty directly from a Uint8Array hash. */
function getPowFromBytes(hash: Uint8Array): number {
let count = 0
for (let i = 0; i < hash.length; i++) {
const byte = hash[i]
if (byte === 0) {
count += 8
} else {
count += Math.clz32(byte) - 24
break
}
}
return count
}
/** /**
* Mine an event with the desired POW. This function mutates the event. * Mine an event with the desired POW. This function mutates the event.
* Note that this operation is synchronous and should be run in a worker context to avoid blocking the main thread. * Note that this operation is synchronous and should be run in a worker context to avoid blocking the main thread.
@@ -43,18 +60,15 @@ export function minePow(unsigned: UnsignedEvent, difficulty: number): Omit<Event
tag[1] = (++count).toString() tag[1] = (++count).toString()
event.id = fastEventHash(event) const hash = sha256(
utf8Encoder.encode(JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content])),
)
if (getPow(event.id) >= difficulty) { if (getPowFromBytes(hash) >= difficulty) {
event.id = bytesToHex(hash)
break break
} }
} }
return event return event
} }
export function fastEventHash(evt: UnsignedEvent): string {
return bytesToHex(
sha256(utf8Encoder.encode(JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]))),
)
}

View File

@@ -85,7 +85,7 @@ test('parse content with hashtags and emoji shortcodes', () => {
['emoji', 'alpaca', 'https://example.com/alpaca.png'], ['emoji', 'alpaca', 'https://example.com/alpaca.png'],
], ],
content: content:
'hey nostr:npub1hpslpc8c5sp3e2nhm2fr7swsfqpys5vyjar5dwpn7e7decps6r8qkcln63 check out :alpaca::alpaca: #alpaca at wss://alpaca.com! :star:', 'hey nostr:npub1hpslpc8c5sp3e2nhm2fr7swsfqpys5vyjar5dwpn7e7decps6r8qkcln63 check out :alpaca::alpaca: #alpaca at wss://alpaca.com! :star:\n\n#WORDS #486 5/6',
created_at: 1234567890, created_at: 1234567890,
pubkey: 'dummy', pubkey: 'dummy',
id: 'dummy', id: 'dummy',
@@ -105,6 +105,11 @@ test('parse content with hashtags and emoji shortcodes', () => {
{ type: 'relay', url: 'wss://alpaca.com/' }, { type: 'relay', url: 'wss://alpaca.com/' },
{ type: 'text', text: '! ' }, { type: 'text', text: '! ' },
{ type: 'emoji', shortcode: 'star', url: 'https://example.com/star.png' }, { type: 'emoji', shortcode: 'star', url: 'https://example.com/star.png' },
{ type: 'text', text: '\n\n' },
{ type: 'hashtag', value: 'WORDS' },
{ type: 'text', text: ' ' },
{ type: 'hashtag', value: '486' },
{ type: 'text', text: ' 5/6' },
]) ])
}) })

View File

@@ -69,7 +69,7 @@ export function* parse(content: string | NostrEvent): Iterable<Block> {
if (u === -1 || (h >= 0 && h < u)) { if (u === -1 || (h >= 0 && h < u)) {
// parse hashtag // parse hashtag
if (h === 0 || content[h - 1] === ' ') { if (h === 0 || content[h - 1].match(noCharacter)) {
const m = content.slice(h + 1, h + MAX_HASHTAG_LENGTH).match(noCharacter) const m = content.slice(h + 1, h + MAX_HASHTAG_LENGTH).match(noCharacter)
const end = m ? h + 1 + m.index! : max const end = m ? h + 1 + m.index! : max
yield { type: 'text', text: content.slice(prevIndex, h) } yield { type: 'text', text: content.slice(prevIndex, h) }

View File

@@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "nostr-tools", "name": "nostr-tools",
"version": "2.22.2", "version": "2.23.0",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -16,71 +16,85 @@
"types": "./lib/types/index.d.ts", "types": "./lib/types/index.d.ts",
"exports": { "exports": {
".": { ".": {
"source": "./index.ts",
"import": "./lib/esm/index.js", "import": "./lib/esm/index.js",
"require": "./lib/cjs/index.js", "require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts" "types": "./lib/types/index.d.ts"
}, },
"./core": { "./core": {
"source": "./core.ts",
"import": "./lib/esm/core.js", "import": "./lib/esm/core.js",
"require": "./lib/cjs/core.js", "require": "./lib/cjs/core.js",
"types": "./lib/types/core.d.ts" "types": "./lib/types/core.d.ts"
}, },
"./pure": { "./pure": {
"source": "./pure.ts",
"import": "./lib/esm/pure.js", "import": "./lib/esm/pure.js",
"require": "./lib/cjs/pure.js", "require": "./lib/cjs/pure.js",
"types": "./lib/types/pure.d.ts" "types": "./lib/types/pure.d.ts"
}, },
"./wasm": { "./wasm": {
"source": "./wasm.ts",
"import": "./lib/esm/wasm.js", "import": "./lib/esm/wasm.js",
"require": "./lib/cjs/wasm.js", "require": "./lib/cjs/wasm.js",
"types": "./lib/types/wasm.d.ts" "types": "./lib/types/wasm.d.ts"
}, },
"./kinds": { "./kinds": {
"source": "./kinds.ts",
"import": "./lib/esm/kinds.js", "import": "./lib/esm/kinds.js",
"require": "./lib/cjs/kinds.js", "require": "./lib/cjs/kinds.js",
"types": "./lib/types/kinds.d.ts" "types": "./lib/types/kinds.d.ts"
}, },
"./filter": { "./filter": {
"source": "./filter.ts",
"import": "./lib/esm/filter.js", "import": "./lib/esm/filter.js",
"require": "./lib/cjs/filter.js", "require": "./lib/cjs/filter.js",
"types": "./lib/types/filter.d.ts" "types": "./lib/types/filter.d.ts"
}, },
"./abstract-relay": { "./abstract-relay": {
"source": "./abstract-relay.ts",
"import": "./lib/esm/abstract-relay.js", "import": "./lib/esm/abstract-relay.js",
"require": "./lib/cjs/abstract-relay.js", "require": "./lib/cjs/abstract-relay.js",
"types": "./lib/types/abstract-relay.d.ts" "types": "./lib/types/abstract-relay.d.ts"
}, },
"./relay": { "./relay": {
"source": "./relay.ts",
"import": "./lib/esm/relay.js", "import": "./lib/esm/relay.js",
"require": "./lib/cjs/relay.js", "require": "./lib/cjs/relay.js",
"types": "./lib/types/relay.d.ts" "types": "./lib/types/relay.d.ts"
}, },
"./abstract-pool": { "./abstract-pool": {
"source": "./abstract-pool.ts",
"import": "./lib/esm/abstract-pool.js", "import": "./lib/esm/abstract-pool.js",
"require": "./lib/cjs/abstract-pool.js", "require": "./lib/cjs/abstract-pool.js",
"types": "./lib/types/abstract-pool.d.ts" "types": "./lib/types/abstract-pool.d.ts"
}, },
"./pool": { "./pool": {
"source": "./pool.ts",
"import": "./lib/esm/pool.js", "import": "./lib/esm/pool.js",
"require": "./lib/cjs/pool.js", "require": "./lib/cjs/pool.js",
"types": "./lib/types/pool.d.ts" "types": "./lib/types/pool.d.ts"
}, },
"./references": { "./references": {
"source": "./references.ts",
"import": "./lib/esm/references.js", "import": "./lib/esm/references.js",
"require": "./lib/cjs/references.js", "require": "./lib/cjs/references.js",
"types": "./lib/types/references.d.ts" "types": "./lib/types/references.d.ts"
}, },
"./nip04": { "./nip04": {
"source": "./nip04.ts",
"import": "./lib/esm/nip04.js", "import": "./lib/esm/nip04.js",
"require": "./lib/cjs/nip04.js", "require": "./lib/cjs/nip04.js",
"types": "./lib/types/nip04.d.ts" "types": "./lib/types/nip04.d.ts"
}, },
"./nip05": { "./nip05": {
"source": "./nip05.ts",
"import": "./lib/esm/nip05.js", "import": "./lib/esm/nip05.js",
"require": "./lib/cjs/nip05.js", "require": "./lib/cjs/nip05.js",
"types": "./lib/types/nip05.d.ts" "types": "./lib/types/nip05.d.ts"
}, },
"./nip06": { "./nip06": {
"source": "./nip06.ts",
"import": "./lib/esm/nip06.js", "import": "./lib/esm/nip06.js",
"require": "./lib/cjs/nip06.js", "require": "./lib/cjs/nip06.js",
"types": "./lib/types/nip06.d.ts" "types": "./lib/types/nip06.d.ts"
@@ -89,146 +103,175 @@
"types": "./lib/types/nip07.d.ts" "types": "./lib/types/nip07.d.ts"
}, },
"./nip10": { "./nip10": {
"source": "./nip10.ts",
"import": "./lib/esm/nip10.js", "import": "./lib/esm/nip10.js",
"require": "./lib/cjs/nip10.js", "require": "./lib/cjs/nip10.js",
"types": "./lib/types/nip10.d.ts" "types": "./lib/types/nip10.d.ts"
}, },
"./nip11": { "./nip11": {
"source": "./nip11.ts",
"import": "./lib/esm/nip11.js", "import": "./lib/esm/nip11.js",
"require": "./lib/cjs/nip11.js", "require": "./lib/cjs/nip11.js",
"types": "./lib/types/nip11.d.ts" "types": "./lib/types/nip11.d.ts"
}, },
"./nip13": { "./nip13": {
"source": "./nip13.ts",
"import": "./lib/esm/nip13.js", "import": "./lib/esm/nip13.js",
"require": "./lib/cjs/nip13.js", "require": "./lib/cjs/nip13.js",
"types": "./lib/types/nip13.d.ts" "types": "./lib/types/nip13.d.ts"
}, },
"./nip17": { "./nip17": {
"source": "./nip17.ts",
"import": "./lib/esm/nip17.js", "import": "./lib/esm/nip17.js",
"require": "./lib/cjs/nip17.js", "require": "./lib/cjs/nip17.js",
"types": "./lib/types/nip17.d.ts" "types": "./lib/types/nip17.d.ts"
}, },
"./nip18": { "./nip18": {
"source": "./nip18.ts",
"import": "./lib/esm/nip18.js", "import": "./lib/esm/nip18.js",
"require": "./lib/cjs/nip18.js", "require": "./lib/cjs/nip18.js",
"types": "./lib/types/nip18.d.ts" "types": "./lib/types/nip18.d.ts"
}, },
"./nip19": { "./nip19": {
"source": "./nip19.ts",
"import": "./lib/esm/nip19.js", "import": "./lib/esm/nip19.js",
"require": "./lib/cjs/nip19.js", "require": "./lib/cjs/nip19.js",
"types": "./lib/types/nip19.d.ts" "types": "./lib/types/nip19.d.ts"
}, },
"./nip21": { "./nip21": {
"source": "./nip21.ts",
"import": "./lib/esm/nip21.js", "import": "./lib/esm/nip21.js",
"require": "./lib/cjs/nip21.js", "require": "./lib/cjs/nip21.js",
"types": "./lib/types/nip21.d.ts" "types": "./lib/types/nip21.d.ts"
}, },
"./nip25": { "./nip25": {
"source": "./nip25.ts",
"import": "./lib/esm/nip25.js", "import": "./lib/esm/nip25.js",
"require": "./lib/cjs/nip25.js", "require": "./lib/cjs/nip25.js",
"types": "./lib/types/nip25.d.ts" "types": "./lib/types/nip25.d.ts"
}, },
"./nip27": { "./nip27": {
"source": "./nip27.ts",
"import": "./lib/esm/nip27.js", "import": "./lib/esm/nip27.js",
"require": "./lib/cjs/nip27.js", "require": "./lib/cjs/nip27.js",
"types": "./lib/types/nip27.d.ts" "types": "./lib/types/nip27.d.ts"
}, },
"./nip28": { "./nip28": {
"source": "./nip28.ts",
"import": "./lib/esm/nip28.js", "import": "./lib/esm/nip28.js",
"require": "./lib/cjs/nip28.js", "require": "./lib/cjs/nip28.js",
"types": "./lib/types/nip28.d.ts" "types": "./lib/types/nip28.d.ts"
}, },
"./nip29": { "./nip29": {
"source": "./nip29.ts",
"import": "./lib/esm/nip29.js", "import": "./lib/esm/nip29.js",
"require": "./lib/cjs/nip29.js", "require": "./lib/cjs/nip29.js",
"types": "./lib/types/nip29.d.ts" "types": "./lib/types/nip29.d.ts"
}, },
"./nip30": { "./nip30": {
"source": "./nip30.ts",
"import": "./lib/esm/nip30.js", "import": "./lib/esm/nip30.js",
"require": "./lib/cjs/nip30.js", "require": "./lib/cjs/nip30.js",
"types": "./lib/types/nip30.d.ts" "types": "./lib/types/nip30.d.ts"
}, },
"./nip39": { "./nip39": {
"source": "./nip39.ts",
"import": "./lib/esm/nip39.js", "import": "./lib/esm/nip39.js",
"require": "./lib/cjs/nip39.js", "require": "./lib/cjs/nip39.js",
"types": "./lib/types/nip39.d.ts" "types": "./lib/types/nip39.d.ts"
}, },
"./nip42": { "./nip42": {
"source": "./nip42.ts",
"import": "./lib/esm/nip42.js", "import": "./lib/esm/nip42.js",
"require": "./lib/cjs/nip42.js", "require": "./lib/cjs/nip42.js",
"types": "./lib/types/nip42.d.ts" "types": "./lib/types/nip42.d.ts"
}, },
"./nip44": { "./nip44": {
"source": "./nip44.ts",
"import": "./lib/esm/nip44.js", "import": "./lib/esm/nip44.js",
"require": "./lib/cjs/nip44.js", "require": "./lib/cjs/nip44.js",
"types": "./lib/types/nip44.d.ts" "types": "./lib/types/nip44.d.ts"
}, },
"./nip46": { "./nip46": {
"source": "./nip46.ts",
"import": "./lib/esm/nip46.js", "import": "./lib/esm/nip46.js",
"require": "./lib/cjs/nip46.js", "require": "./lib/cjs/nip46.js",
"types": "./lib/types/nip46.d.ts" "types": "./lib/types/nip46.d.ts"
}, },
"./nip49": { "./nip49": {
"source": "./nip49.ts",
"import": "./lib/esm/nip49.js", "import": "./lib/esm/nip49.js",
"require": "./lib/cjs/nip49.js", "require": "./lib/cjs/nip49.js",
"types": "./lib/types/nip49.d.ts" "types": "./lib/types/nip49.d.ts"
}, },
"./nip54": { "./nip54": {
"source": "./nip54.ts",
"import": "./lib/esm/nip54.js", "import": "./lib/esm/nip54.js",
"require": "./lib/cjs/nip54.js", "require": "./lib/cjs/nip54.js",
"types": "./lib/types/nip54.d.ts" "types": "./lib/types/nip54.d.ts"
}, },
"./nip57": { "./nip57": {
"source": "./nip57.ts",
"import": "./lib/esm/nip57.js", "import": "./lib/esm/nip57.js",
"require": "./lib/cjs/nip57.js", "require": "./lib/cjs/nip57.js",
"types": "./lib/types/nip57.d.ts" "types": "./lib/types/nip57.d.ts"
}, },
"./nip59": { "./nip59": {
"source": "./nip59.ts",
"import": "./lib/esm/nip59.js", "import": "./lib/esm/nip59.js",
"require": "./lib/cjs/nip59.js", "require": "./lib/cjs/nip59.js",
"types": "./lib/types/nip59.d.ts" "types": "./lib/types/nip59.d.ts"
}, },
"./nip58": { "./nip58": {
"source": "./nip58.ts",
"import": "./lib/esm/nip58.js", "import": "./lib/esm/nip58.js",
"require": "./lib/cjs/nip58.js", "require": "./lib/cjs/nip58.js",
"types": "./lib/types/nip58.d.ts" "types": "./lib/types/nip58.d.ts"
}, },
"./nip75": { "./nip75": {
"source": "./nip75.ts",
"import": "./lib/esm/nip75.js", "import": "./lib/esm/nip75.js",
"require": "./lib/cjs/nip75.js", "require": "./lib/cjs/nip75.js",
"types": "./lib/types/nip75.d.ts" "types": "./lib/types/nip75.d.ts"
}, },
"./nip94": { "./nip94": {
"source": "./nip94.ts",
"import": "./lib/esm/nip94.js", "import": "./lib/esm/nip94.js",
"require": "./lib/cjs/nip94.js", "require": "./lib/cjs/nip94.js",
"types": "./lib/types/nip94.d.ts" "types": "./lib/types/nip94.d.ts"
}, },
"./nip98": { "./nip98": {
"source": "./nip98.ts",
"import": "./lib/esm/nip98.js", "import": "./lib/esm/nip98.js",
"require": "./lib/cjs/nip98.js", "require": "./lib/cjs/nip98.js",
"types": "./lib/types/nip98.d.ts" "types": "./lib/types/nip98.d.ts"
}, },
"./nip99": { "./nip99": {
"source": "./nip99.ts",
"import": "./lib/esm/nip99.js", "import": "./lib/esm/nip99.js",
"require": "./lib/cjs/nip99.js", "require": "./lib/cjs/nip99.js",
"types": "./lib/types/nip99.d.ts" "types": "./lib/types/nip99.d.ts"
}, },
"./nipb7": { "./nipb7": {
"source": "./nipb7.ts",
"import": "./lib/esm/nipb7.js", "import": "./lib/esm/nipb7.js",
"require": "./lib/cjs/nipb7.js", "require": "./lib/cjs/nipb7.js",
"types": "./lib/types/nipb7.d.ts" "types": "./lib/types/nipb7.d.ts"
}, },
"./fakejson": { "./fakejson": {
"source": "./fakejson.ts",
"import": "./lib/esm/fakejson.js", "import": "./lib/esm/fakejson.js",
"require": "./lib/cjs/fakejson.js", "require": "./lib/cjs/fakejson.js",
"types": "./lib/types/fakejson.d.ts" "types": "./lib/types/fakejson.d.ts"
}, },
"./signer": { "./signer": {
"source": "./signer.ts",
"import": "./lib/esm/signer.js", "import": "./lib/esm/signer.js",
"require": "./lib/cjs/signer.js", "require": "./lib/cjs/signer.js",
"types": "./lib/types/signer.d.ts" "types": "./lib/types/signer.d.ts"
}, },
"./utils": { "./utils": {
"source": "./utils.ts",
"import": "./lib/esm/utils.js", "import": "./lib/esm/utils.js",
"require": "./lib/cjs/utils.js", "require": "./lib/cjs/utils.js",
"types": "./lib/types/utils.d.ts" "types": "./lib/types/utils.d.ts"

View File

@@ -1,7 +1,6 @@
import { describe, test, expect } from 'bun:test' import { describe, test, expect } from 'bun:test'
import { buildEvent } from './test-helpers.ts' import { buildEvent } from './test-helpers.ts'
import { import {
Queue,
insertEventIntoAscendingList, insertEventIntoAscendingList,
insertEventIntoDescendingList, insertEventIntoDescendingList,
binarySearch, binarySearch,
@@ -221,48 +220,6 @@ describe('inserting into a asc sorted list of events', () => {
}) })
}) })
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', () => { 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 => ('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 => ('x' < b ? -1 : 'x' === b ? 0 : 1))).toEqual([4, false])

View File

@@ -123,62 +123,3 @@ export function mergeReverseSortedLists(list1: NostrEvent[], list2: NostrEvent[]
return result return result
} }
export class QueueNode<V> {
public value: V
public next: QueueNode<V> | null = null
public prev: QueueNode<V> | null = null
constructor(message: V) {
this.value = message
}
}
export class Queue<V> {
public first: QueueNode<V> | null
public last: QueueNode<V> | null
constructor() {
this.first = null
this.last = null
}
enqueue(value: V): boolean {
const newNode = new QueueNode(value)
if (!this.last) {
// list is empty
this.first = newNode
this.last = newNode
} else if (this.last === this.first) {
// list has a single element
this.last = newNode
this.last.prev = this.first
this.first.next = newNode
} else {
// list has elements, add as last
newNode.prev = this.last
this.last.next = newNode
this.last = newNode
}
return true
}
dequeue(): V | null {
if (!this.first) return null
if (this.first === this.last) {
const target = this.first
this.first = null
this.last = null
return target.value
}
const target = this.first
this.first = target.next
if (this.first) {
this.first.prev = null // fix: clean up prev pointer
}
return target.value
}
}