Compare commits

...

17 Commits

Author SHA1 Message Date
ffaex
32fd25556b added new event kind 1063
see https://github.com/nostr-protocol/nips/blob/master/94.md
2023-08-21 15:03:23 -03:00
Sepehr Safari
0925f5db81 add batchedList method to SimplePool 2023-08-21 10:44:33 -03:00
fiatjaf
bce976fecd get rid of httpmethod enum. 2023-08-16 14:07:26 -03:00
fiatjaf
45e479d7aa let it throw. 2023-08-16 13:59:31 -03:00
Jon Staab
b92407b156 nip44 updates (#278)
Co-authored-by: Jonathan Staab <shtaab@gmail.com>
2023-08-16 13:53:37 -03:00
Pierre Buyle
2431896921 fix(nip98): Add support for HEAD, PUT, CONNECT, OPTIONS, TRACE and PATCH http methods
This PR adds common HTTP methods (as listed on https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
2023-08-15 11:03:47 -03:00
Jonathan Staab
d13eecad4a Add support for nip44 2023-08-12 20:46:32 -03:00
Alex Gleason
df6f887d7e Event, Filter: allow any kind number
Fixes https://github.com/nbd-wtf/nostr-tools/issues/275
2023-08-12 20:13:53 -03:00
Alex Gleason
e00362e7c9 Filter: let tag queries be undefined 2023-08-12 16:30:24 -03:00
fiatjaf
9efdd16e26 fix check for undefined ws
fixes https://github.com/nbd-wtf/nostr-tools/issues/271
2023-08-11 07:09:40 -03:00
Alex Gleason
de7e128818 Merge pull request #267 from Airtune/nip98-extract-pubkey
+nip98.unpackEventFromToken +nip98.validateEvent
2023-08-08 08:48:29 -05:00
Airtune
4978c858e7 Update nip98.ts examples 2023-08-08 02:45:23 -04:00
Airtune
16c7ae2a70 +nip98.unpackEventFromToken +nip98.validateEvent 2023-08-07 22:16:23 -04:00
fiatjaf
3368e8c00e bump minor version because of the breaking change on publish()
yes, I don't understand semver
2023-07-31 23:05:36 -03:00
Airtune
e5a3ad9855 Export nip28 functions in index.ts and bump version (#265) 2023-07-31 23:04:45 -03:00
Airtune
03185c654b Create nip28.ts and nip28.test.ts (#264) 2023-07-31 08:29:45 -03:00
fiatjaf
9d690814ca turn .publish() into a normal async function returning a promise.
this simplifies the code and makes the API more intuitive.

we used to need the event emitter thing because we were subscribing to the same relay
to check if the event had been published, but that is not necessary now that we assume
an OK response will always come.

closes https://github.com/nbd-wtf/nostr-tools/issues/262
2023-07-30 18:23:05 -03:00
15 changed files with 1206 additions and 890 deletions

View File

@@ -108,13 +108,7 @@ let event = {
event.id = getEventHash(event)
event.sig = getSignature(event, sk)
let pub = relay.publish(event)
pub.on('ok', () => {
console.log(`${relay.url} has accepted our event`)
})
pub.on('failed', reason => {
console.log(`failed to publish to ${relay.url}: ${reason}`)
})
await relay.publish(event)
let events = await relay.list([{kinds: [0, 1]}])
let event = await relay.get({

View File

@@ -5,6 +5,7 @@ import {bytesToHex} from '@noble/hashes/utils'
import {getPublicKey} from './keys.ts'
import {utf8Encoder} from './utils.ts'
/** @deprecated Use numbers instead. */
/* eslint-disable no-unused-vars */
export enum Kind {
Metadata = 0,
@@ -30,21 +31,22 @@ export enum Kind {
HttpAuth = 27235,
ProfileBadge = 30008,
BadgeDefinition = 30009,
Article = 30023
Article = 30023,
FileMetadata = 1063
}
export type EventTemplate<K extends number = Kind> = {
export type EventTemplate<K extends number = number> = {
kind: K
tags: string[][]
content: string
created_at: number
}
export type UnsignedEvent<K extends number = Kind> = EventTemplate<K> & {
export type UnsignedEvent<K extends number = number> = EventTemplate<K> & {
pubkey: string
}
export type Event<K extends number = Kind> = UnsignedEvent<K> & {
export type Event<K extends number = number> = UnsignedEvent<K> & {
id: string
sig: string
}
@@ -60,7 +62,7 @@ export function getBlankEvent<K>(kind: K | Kind.Blank = Kind.Blank) {
}
}
export function finishEvent<K extends number = Kind>(
export function finishEvent<K extends number = number>(
t: EventTemplate<K>,
privateKey: string
): Event<K> {

View File

@@ -1,6 +1,6 @@
import {Event, type Kind} from './event.ts'
export type Filter<K extends number = Kind> = {
export type Filter<K extends number = number> = {
ids?: string[]
kinds?: K[]
authors?: string[]
@@ -8,7 +8,7 @@ export type Filter<K extends number = Kind> = {
until?: number
limit?: number
search?: string
[key: `#${string}`]: string[]
[key: `#${string}`]: string[] | undefined
}
export function matchFilter(
@@ -34,7 +34,7 @@ export function matchFilter(
if (
values &&
!event.tags.find(
([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1
([t, v]) => t === f.slice(1) && values!.indexOf(v) !== -1
)
)
return false

View File

@@ -16,8 +16,10 @@ export * as nip21 from './nip21.ts'
export * as nip25 from './nip25.ts'
export * as nip26 from './nip26.ts'
export * as nip27 from './nip27.ts'
export * as nip28 from './nip28.ts'
export * as nip39 from './nip39.ts'
export * as nip42 from './nip42.ts'
export * as nip44 from './nip44.ts'
export * as nip57 from './nip57.ts'
export * as nip98 from './nip98.ts'

134
nip28.test.ts Normal file
View File

@@ -0,0 +1,134 @@
import {Kind} from './event.ts'
import {getPublicKey} from './keys.ts'
import {
channelCreateEvent,
channelMetadataEvent,
channelMessageEvent,
channelHideMessageEvent,
channelMuteUserEvent,
ChannelMetadata,
ChannelMessageEventTemplate
} from './nip28.ts'
const privateKey =
'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf'
const publicKey = getPublicKey(privateKey)
describe('NIP-28 Functions', () => {
const channelMetadata: ChannelMetadata = {
name: 'Test Channel',
about: 'This is a test channel',
picture: 'https://example.com/picture.jpg'
}
it('channelCreateEvent should create an event with given template', () => {
const template = {
content: channelMetadata,
created_at: 1617932115
}
const event = channelCreateEvent(template, privateKey)
expect(event!.kind).toEqual(Kind.ChannelCreation)
expect(event!.content).toEqual(JSON.stringify(template.content))
expect(event!.pubkey).toEqual(publicKey)
})
it('channelMetadataEvent should create a signed event with given template', () => {
const template = {
channel_create_event_id: 'channel creation event id',
content: channelMetadata,
created_at: 1617932115
}
const event = channelMetadataEvent(template, privateKey)
expect(event!.kind).toEqual(Kind.ChannelMetadata)
expect(event!.tags).toEqual([['e', template.channel_create_event_id]])
expect(event!.content).toEqual(JSON.stringify(template.content))
expect(event!.pubkey).toEqual(publicKey)
expect(typeof event!.id).toEqual('string')
expect(typeof event!.sig).toEqual('string')
})
it('channelMessageEvent should create a signed message event with given template', () => {
const template = {
channel_create_event_id: 'channel creation event id',
relay_url: 'https://relay.example.com',
content: 'Hello, world!',
created_at: 1617932115
}
const event = channelMessageEvent(template, privateKey)
expect(event.kind).toEqual(Kind.ChannelMessage)
expect(event.tags[0]).toEqual([
'e',
template.channel_create_event_id,
template.relay_url,
'root'
])
expect(event.content).toEqual(template.content)
expect(event.pubkey).toEqual(publicKey)
expect(typeof event.id).toEqual('string')
expect(typeof event.sig).toEqual('string')
})
it('channelMessageEvent should create a signed message reply event with given template', () => {
const template: ChannelMessageEventTemplate = {
channel_create_event_id: 'channel creation event id',
reply_to_channel_message_event_id: 'channel message event id',
relay_url: 'https://relay.example.com',
content: 'Hello, world!',
created_at: 1617932115
}
const event = channelMessageEvent(template, privateKey)
expect(event.kind).toEqual(Kind.ChannelMessage)
expect(event.tags).toContainEqual([
'e',
template.channel_create_event_id,
template.relay_url,
'root'
])
expect(event.tags).toContainEqual([
'e',
template.reply_to_channel_message_event_id,
template.relay_url,
'reply'
])
expect(event.content).toEqual(template.content)
expect(event.pubkey).toEqual(publicKey)
expect(typeof event.id).toEqual('string')
expect(typeof event.sig).toEqual('string')
})
it('channelHideMessageEvent should create a signed event with given template', () => {
const template = {
channel_message_event_id: 'channel message event id',
content: {reason: 'Inappropriate content'},
created_at: 1617932115
}
const event = channelHideMessageEvent(template, privateKey)
expect(event!.kind).toEqual(Kind.ChannelHideMessage)
expect(event!.tags).toEqual([['e', template.channel_message_event_id]])
expect(event!.content).toEqual(JSON.stringify(template.content))
expect(event!.pubkey).toEqual(publicKey)
expect(typeof event!.id).toEqual('string')
expect(typeof event!.sig).toEqual('string')
})
it('channelMuteUserEvent should create a signed event with given template', () => {
const template = {
content: {reason: 'Spamming'},
created_at: 1617932115,
pubkey_to_mute: 'pubkey to mute'
}
const event = channelMuteUserEvent(template, privateKey)
expect(event!.kind).toEqual(Kind.ChannelMuteUser)
expect(event!.tags).toEqual([['p', template.pubkey_to_mute]])
expect(event!.content).toEqual(JSON.stringify(template.content))
expect(event!.pubkey).toEqual(publicKey)
expect(typeof event!.id).toEqual('string')
expect(typeof event!.sig).toEqual('string')
})
})

163
nip28.ts Normal file
View File

@@ -0,0 +1,163 @@
import {Event, finishEvent, Kind} from './event.ts'
export interface ChannelMetadata {
name: string
about: string
picture: string
}
export interface ChannelCreateEventTemplate {
/* JSON string containing ChannelMetadata as defined for Kind 40 and 41 in nip-28. */
content: string | ChannelMetadata
created_at: number
tags?: string[][]
}
export interface ChannelMetadataEventTemplate {
channel_create_event_id: string
/* JSON string containing ChannelMetadata as defined for Kind 40 and 41 in nip-28. */
content: string | ChannelMetadata
created_at: number
tags?: string[][]
}
export interface ChannelMessageEventTemplate {
channel_create_event_id: string
reply_to_channel_message_event_id?: string
relay_url: string
content: string
created_at: number
tags?: string[][]
}
export interface ChannelHideMessageEventTemplate {
channel_message_event_id: string
content: string | {reason: string}
created_at: number
tags?: string[][]
}
export interface ChannelMuteUserEventTemplate {
content: string | {reason: string}
created_at: number
pubkey_to_mute: string
tags?: string[][]
}
export const channelCreateEvent = (
t: ChannelCreateEventTemplate,
privateKey: string
): Event<Kind.ChannelCreation> | undefined => {
let content: string
if (typeof t.content === 'object') {
content = JSON.stringify(t.content)
} else if (typeof t.content === 'string') {
content = t.content
} else {
return undefined
}
return finishEvent(
{
kind: Kind.ChannelCreation,
tags: [...(t.tags ?? [])],
content: content,
created_at: t.created_at
},
privateKey
)
}
export const channelMetadataEvent = (
t: ChannelMetadataEventTemplate,
privateKey: string
): Event<Kind.ChannelMetadata> | undefined => {
let content: string
if (typeof t.content === 'object') {
content = JSON.stringify(t.content)
} else if (typeof t.content === 'string') {
content = t.content
} else {
return undefined
}
return finishEvent(
{
kind: Kind.ChannelMetadata,
tags: [['e', t.channel_create_event_id], ...(t.tags ?? [])],
content: content,
created_at: t.created_at
},
privateKey
)
}
export const channelMessageEvent = (
t: ChannelMessageEventTemplate,
privateKey: string
): Event<Kind.ChannelMessage> => {
const tags = [['e', t.channel_create_event_id, t.relay_url, 'root']]
if (t.reply_to_channel_message_event_id) {
tags.push(['e', t.reply_to_channel_message_event_id, t.relay_url, 'reply'])
}
return finishEvent(
{
kind: Kind.ChannelMessage,
tags: [...tags, ...(t.tags ?? [])],
content: t.content,
created_at: t.created_at
},
privateKey
)
}
/* "e" tag should be the kind 42 event to hide */
export const channelHideMessageEvent = (
t: ChannelHideMessageEventTemplate,
privateKey: string
): Event<Kind.ChannelHideMessage> | undefined => {
let content: string
if (typeof t.content === 'object') {
content = JSON.stringify(t.content)
} else if (typeof t.content === 'string') {
content = t.content
} else {
return undefined
}
return finishEvent(
{
kind: Kind.ChannelHideMessage,
tags: [['e', t.channel_message_event_id], ...(t.tags ?? [])],
content: content,
created_at: t.created_at
},
privateKey
)
}
export const channelMuteUserEvent = (
t: ChannelMuteUserEventTemplate,
privateKey: string
): Event<Kind.ChannelMuteUser> | undefined => {
let content: string
if (typeof t.content === 'object') {
content = JSON.stringify(t.content)
} else if (typeof t.content === 'string') {
content = t.content
} else {
return undefined
}
return finishEvent(
{
kind: Kind.ChannelMuteUser,
tags: [['p', t.pubkey_to_mute], ...(t.tags ?? [])],
content: content,
created_at: t.created_at
},
privateKey
)
}

View File

@@ -17,7 +17,9 @@ export const authenticate = async ({
}: {
challenge: string
relay: Relay
sign: <K extends number = number>(e: EventTemplate<K>) => Promise<Event<K>> | Event<K>
sign: <K extends number = number>(
e: EventTemplate<K>
) => Promise<Event<K>> | Event<K>
}): Promise<void> => {
const e: EventTemplate = {
kind: Kind.ClientAuth,
@@ -28,15 +30,5 @@ export const authenticate = async ({
],
content: ''
}
const pub = relay.auth(await sign(e))
return new Promise((resolve, reject) => {
pub.on('ok', function ok() {
pub.off('ok', ok)
resolve()
})
pub.on('failed', function fail(reason: string) {
pub.off('failed', fail)
reject(reason)
})
})
return relay.auth(await sign(e))
}

21
nip44.test.ts Normal file
View File

@@ -0,0 +1,21 @@
import crypto from 'node:crypto'
import {hexToBytes} from '@noble/hashes/utils'
import {encrypt, decrypt, getSharedSecret} from './nip44.ts'
import {getPublicKey, generatePrivateKey} from './keys.ts'
// @ts-ignore
// eslint-disable-next-line no-undef
globalThis.crypto = crypto
test('encrypt and decrypt message', async () => {
let sk1 = generatePrivateKey()
let sk2 = generatePrivateKey()
let pk1 = getPublicKey(sk1)
let pk2 = getPublicKey(sk2)
let sharedKey1 = getSharedSecret(sk1, pk2)
let sharedKey2 = getSharedSecret(sk2, pk1)
expect(decrypt(hexToBytes(sk1), encrypt(hexToBytes(sk1), 'hello'))).toEqual('hello')
expect(decrypt(sharedKey2, encrypt(sharedKey1, 'hello'))).toEqual('hello')
})

40
nip44.ts Normal file
View File

@@ -0,0 +1,40 @@
import {base64} from '@scure/base'
import {randomBytes} from '@noble/hashes/utils'
import {secp256k1} from '@noble/curves/secp256k1'
import {sha256} from '@noble/hashes/sha256'
import {xchacha20} from '@noble/ciphers/chacha'
import {utf8Decoder, utf8Encoder} from './utils.ts'
export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array =>
sha256(secp256k1.getSharedSecret(privkey, '02' + pubkey).subarray(1, 33))
export function encrypt(key: Uint8Array, text: string, v = 1) {
if (v !== 1) {
throw new Error('NIP44: unknown encryption version')
}
const nonce = randomBytes(24)
const plaintext = utf8Encoder.encode(text)
const ciphertext = xchacha20(key, nonce, plaintext)
const payload = new Uint8Array(25 + ciphertext.length)
payload.set([v], 0)
payload.set(nonce, 1)
payload.set(ciphertext, 25)
return base64.encode(payload)
}
export function decrypt(key: Uint8Array, payload: string) {
let data = base64.decode(payload)
if (data[0] !== 1) {
throw new Error(`NIP44: unknown encryption version: ${data[0]}`)
}
const nonce = data.slice(1, 25)
const ciphertext = data.slice(25)
const plaintext = xchacha20(key, nonce, ciphertext)
return utf8Decoder.decode(plaintext)
}

View File

@@ -1,7 +1,5 @@
import {base64} from '@scure/base'
import {getToken, validateToken} from './nip98.ts'
import {getToken, unpackEventFromToken, validateEvent, validateToken} from './nip98.ts'
import {Event, Kind, finishEvent} from './event.ts'
import {utf8Decoder} from './utils.ts'
import {generatePrivateKey, getPublicKey} from './keys.ts'
const sk = generatePrivateKey()
@@ -12,9 +10,7 @@ describe('getToken', () => {
finishEvent(e, sk)
)
const decodedResult: Event = JSON.parse(
utf8Decoder.decode(base64.decode(result))
)
const decodedResult: Event = await unpackEventFromToken(result)
expect(decodedResult.created_at).toBeGreaterThan(0)
expect(decodedResult.content).toBe('')
@@ -31,9 +27,7 @@ describe('getToken', () => {
finishEvent(e, sk)
)
const decodedResult: Event = JSON.parse(
utf8Decoder.decode(base64.decode(result))
)
const decodedResult: Event = await unpackEventFromToken(result)
expect(decodedResult.created_at).toBeGreaterThan(0)
expect(decodedResult.content).toBe('')
@@ -57,9 +51,7 @@ describe('getToken', () => {
expect(result.startsWith(authorizationScheme)).toBe(true)
const decodedResult: Event = JSON.parse(
utf8Decoder.decode(base64.decode(result.replace(authorizationScheme, '')))
)
const decodedResult: Event = await unpackEventFromToken(result)
expect(decodedResult.created_at).toBeGreaterThan(0)
expect(decodedResult.content).toBe('')
@@ -136,4 +128,43 @@ describe('validateToken', () => {
const result = validateToken(validToken, 'http://test.com', 'post')
await expect(result).rejects.toThrow(Error)
})
test('validateEvent returns true for valid decoded token with authorization scheme', async () => {
const validToken = await getToken(
'http://test.com',
'get',
e => finishEvent(e, sk),
true
)
const decodedResult: Event = await unpackEventFromToken(validToken)
const result = await validateEvent(decodedResult, 'http://test.com', 'get')
expect(result).toBe(true)
})
test('validateEvent throws an error for a wrong url', async () => {
const validToken = await getToken(
'http://test.com',
'get',
e => finishEvent(e, sk),
true
)
const decodedResult: Event = await unpackEventFromToken(validToken)
const result = validateEvent(decodedResult, 'http://wrong-test.com', 'get')
await expect(result).rejects.toThrow(Error)
})
test('validateEvent throws an error for a wrong method', async () => {
const validToken = await getToken(
'http://test.com',
'get',
e => finishEvent(e, sk),
true
)
const decodedResult: Event = await unpackEventFromToken(validToken)
const result = validateEvent(decodedResult, 'http://test.com', 'post')
await expect(result).rejects.toThrow(Error)
})
})

View File

@@ -8,11 +8,6 @@ import {
} from './event'
import {utf8Decoder, utf8Encoder} from './utils'
enum HttpMethod {
Get = 'get',
Post = 'post'
}
const _authorizationScheme = 'Nostr '
/**
@@ -20,11 +15,11 @@ const _authorizationScheme = 'Nostr '
*
* @example
* const sign = window.nostr.signEvent
* await getToken('https://example.com/login', 'post', sign, true)
* await nip98.getToken('https://example.com/login', 'post', (e) => sign(e), true)
*/
export async function getToken(
loginUrl: string,
httpMethod: HttpMethod | string,
httpMethod: string,
sign: <K extends number = number>(
e: EventTemplate<K>
) => Promise<Event<K>> | Event<K>,
@@ -32,8 +27,6 @@ export async function getToken(
): Promise<string> {
if (!loginUrl || !httpMethod)
throw new Error('Missing loginUrl or httpMethod')
if (httpMethod !== HttpMethod.Get && httpMethod !== HttpMethod.Post)
throw new Error('Unknown httpMethod')
const event = getBlankEvent(Kind.HttpAuth)
@@ -58,13 +51,20 @@ export async function getToken(
* Validate token for NIP-98 flow.
*
* @example
* await validateToken('Nostr base64token', 'https://example.com/login', 'post')
* await nip98.validateToken('Nostr base64token', 'https://example.com/login', 'post')
*/
export async function validateToken(
token: string,
url: string,
method: string
): Promise<boolean> {
const event = await unpackEventFromToken(token).catch((error) => { throw(error) })
const valid = await validateEvent(event, url, method).catch((error) => { throw(error) })
return valid
}
export async function unpackEventFromToken(token: string): Promise<Event> {
if (!token) {
throw new Error('Missing token')
}
@@ -76,6 +76,15 @@ export async function validateToken(
}
const event = JSON.parse(eventB64) as Event
return event
}
export async function validateEvent(
event: Event,
url: string,
method: string
): Promise<boolean> {
if (!event) {
throw new Error('Invalid nostr event')
}

View File

@@ -1,6 +1,6 @@
{
"name": "nostr-tools",
"version": "1.13.1",
"version": "1.14.2",
"description": "Tools for making a Nostr client.",
"repository": {
"type": "git",
@@ -21,6 +21,7 @@
"dependencies": {
"@noble/curves": "1.1.0",
"@noble/hashes": "1.3.1",
"@noble/ciphers": "^0.2.0",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"

175
pool.ts
View File

@@ -1,27 +1,44 @@
import {
relayInit,
type Pub,
type Relay,
type Sub,
type SubscriptionOptions,
type SubscriptionOptions
} from './relay.ts'
import {normalizeURL} from './utils.ts'
import type {Event} from './event.ts'
import type {Filter} from './filter.ts'
import {matchFilters, type Filter} from './filter.ts'
type BatchedRequest = {
filters: Filter<any>[]
relays: string[]
resolve: (events: Event<any>[]) => void
events: Event<any>[]
}
export class SimplePool {
private _conn: {[url: string]: Relay}
private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay
private batchedByKey: {[batchKey: string]: BatchedRequest[]} = {}
private eoseSubTimeout: number
private getTimeout: number
private seenOnEnabled: boolean = true
private batchInterval: number = 100
constructor(options: {eoseSubTimeout?: number; getTimeout?: number; seenOnEnabled?: boolean} = {}) {
constructor(
options: {
eoseSubTimeout?: number
getTimeout?: number
seenOnEnabled?: boolean
batchInterval?: number
} = {}
) {
this._conn = {}
this.eoseSubTimeout = options.eoseSubTimeout || 3400
this.getTimeout = options.getTimeout || 3400
this.seenOnEnabled = options.seenOnEnabled !== false
this.batchInterval = options.batchInterval || 100
}
close(relays: string[]): void {
@@ -46,7 +63,11 @@ export class SimplePool {
return relay
}
sub<K extends number = number>(relays: string[], filters: Filter<K>[], opts?: SubscriptionOptions): Sub<K> {
sub<K extends number = number>(
relays: string[],
filters: Filter<K>[],
opts?: SubscriptionOptions
): Sub<K> {
let _knownIds: Set<string> = new Set()
let modifiedOpts = {...(opts || {})}
modifiedOpts.alreadyHaveEvent = (id, url) => {
@@ -72,34 +93,36 @@ export class SimplePool {
for (let cb of eoseListeners.values()) cb()
}, this.eoseSubTimeout)
relays.forEach(async relay => {
let r
try {
r = await this.ensureRelay(relay)
} catch (err) {
handleEose()
return
}
if (!r) return
let s = r.sub(filters, modifiedOpts)
s.on('event', (event) => {
_knownIds.add(event.id as string)
for (let cb of eventListeners.values()) cb(event)
})
s.on('eose', () => {
if (eoseSent) return
handleEose()
})
subs.push(s)
function handleEose() {
eosesMissing--
if (eosesMissing === 0) {
clearTimeout(eoseTimeout)
for (let cb of eoseListeners.values()) cb()
relays
.filter((r, i, a) => a.indexOf(r) == i)
.forEach(async relay => {
let r
try {
r = await this.ensureRelay(relay)
} catch (err) {
handleEose()
return
}
}
})
if (!r) return
let s = r.sub(filters, modifiedOpts)
s.on('event', event => {
_knownIds.add(event.id as string)
for (let cb of eventListeners.values()) cb(event)
})
s.on('eose', () => {
if (eoseSent) return
handleEose()
})
subs.push(s)
function handleEose() {
eosesMissing--
if (eosesMissing === 0) {
clearTimeout(eoseTimeout)
for (let cb of eoseListeners.values()) cb()
}
}
})
let greaterSub: Sub = {
sub(filters, opts) {
@@ -138,7 +161,7 @@ export class SimplePool {
sub.unsub()
resolve(null)
}, this.getTimeout)
sub.on('event', (event) => {
sub.on('event', event => {
resolve(event)
clearTimeout(timeout)
sub.unsub()
@@ -155,7 +178,7 @@ export class SimplePool {
let events: Event<K>[] = []
let sub = this.sub(relays, filters, opts)
sub.on('event', (event) => {
sub.on('event', event => {
events.push(event)
})
@@ -167,39 +190,63 @@ export class SimplePool {
})
}
publish(relays: string[], event: Event<number>): Pub {
const pubPromises: Promise<Pub>[] = relays.map(async relay => {
let r
try {
r = await this.ensureRelay(relay)
return r.publish(event)
} catch (_) {
return {on() {}, off() {}}
batchedList<K extends number = number>(
batchKey: string,
relays: string[],
filters: Filter<K>[]
): Promise<Event<K>[]> {
return new Promise(resolve => {
if (!this.batchedByKey[batchKey]) {
this.batchedByKey[batchKey] = [
{
filters,
relays,
resolve,
events: []
}
]
setTimeout(() => {
Object.keys(this.batchedByKey).forEach(async batchKey => {
const batchedRequests = this.batchedByKey[batchKey]
const filters = [] as Filter[]
const relays = [] as string[]
batchedRequests.forEach(br => {
filters.push(...br.filters)
relays.push(...br.relays)
})
const sub = this.sub(relays, filters)
sub.on('event', event => {
batchedRequests.forEach(
br => matchFilters(br.filters, event) && br.events.push(event)
)
})
sub.on('eose', () => {
sub.unsub()
batchedRequests.forEach(br => br.resolve(br.events))
})
delete this.batchedByKey[batchKey]
})
}, this.batchInterval)
} else {
this.batchedByKey[batchKey].push({
filters,
relays,
resolve,
events: []
})
}
})
}
const callbackMap = new Map()
return {
on(type, cb) {
relays.forEach(async (relay, i) => {
let pub = await pubPromises[i]
let callback = () => cb(relay)
callbackMap.set(cb, callback)
pub.on(type, callback)
})
},
off(type, cb) {
relays.forEach(async (_, i) => {
let callback = callbackMap.get(cb)
if (callback) {
let pub = await pubPromises[i]
pub.off(type, callback)
}
})
}
}
publish(relays: string[], event: Event<number>): Promise<void>[] {
return relays.map(async relay => {
let r = await this.ensureRelay(relay)
return r.publish(event)
})
}
seenOn(id: string): string[] {

View File

@@ -3,7 +3,7 @@
import {verifySignature, validateEvent, type Event} from './event.ts'
import {matchFilters, type Filter} from './filter.ts'
import {getHex64, getSubscriptionId} from './fakejson.ts'
import { MessageQueue } from './utils.ts'
import {MessageQueue} from './utils.ts'
type RelayEvent = {
connect: () => void | Promise<void>
@@ -25,15 +25,24 @@ export type Relay = {
status: number
connect: () => Promise<void>
close: () => void
sub: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Sub<K>
list: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Promise<Event<K>[]>
get: <K extends number = number>(filter: Filter<K>, opts?: SubscriptionOptions) => Promise<Event<K> | null>
sub: <K extends number = number>(
filters: Filter<K>[],
opts?: SubscriptionOptions
) => Sub<K>
list: <K extends number = number>(
filters: Filter<K>[],
opts?: SubscriptionOptions
) => Promise<Event<K>[]>
get: <K extends number = number>(
filter: Filter<K>,
opts?: SubscriptionOptions
) => Promise<Event<K> | null>
count: (
filters: Filter[],
opts?: SubscriptionOptions
) => Promise<CountPayload | null>
publish: (event: Event<number>) => Pub
auth: (event: Event<number>) => Pub
publish: (event: Event<number>) => Promise<void>
auth: (event: Event<number>) => Promise<void>
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
event: T,
listener: U
@@ -43,12 +52,11 @@ export type Relay = {
listener: U
) => void
}
export type Pub = {
on: (type: 'ok' | 'failed', cb: any) => void
off: (type: 'ok' | 'failed', cb: any) => void
}
export type Sub<K extends number = number> = {
sub: <K extends number = number>(filters: Filter<K>[], opts: SubscriptionOptions) => Sub<K>
sub: <K extends number = number>(
filters: Filter<K>[],
opts: SubscriptionOptions
) => Sub<K>
unsub: () => void
on: <T extends keyof SubEvent<K>, U extends SubEvent<K>[T]>(
event: T,
@@ -93,9 +101,8 @@ export function relayInit(
} = {}
var pubListeners: {
[eventid: string]: {
ok: Array<() => void>
seen: Array<() => void>
failed: Array<(reason: string) => void>
resolve: (_: unknown) => void
reject: (err: Error) => void
}
} = {}
@@ -196,10 +203,9 @@ export function relayInit(
let ok: boolean = data[2]
let reason: string = data[3] || ''
if (id in pubListeners) {
if (ok) pubListeners[id].ok.forEach(cb => cb())
else pubListeners[id].failed.forEach(cb => cb(reason))
pubListeners[id].ok = [] // 'ok' only happens once per pub, so stop listeners here
pubListeners[id].failed = []
let {resolve, reject} = pubListeners[id]
if (ok) resolve(null)
else reject(new Error(reason))
}
return
}
@@ -294,26 +300,16 @@ export function relayInit(
}
function _publishEvent(event: Event<number>, type: string) {
if (!event.id) throw new Error(`event ${event} has no id`)
let id = event.id
trySend([type, event])
return {
on: (type: 'ok' | 'failed', cb: any) => {
pubListeners[id] = pubListeners[id] || {
ok: [],
failed: []
}
pubListeners[id][type].push(cb)
},
off: (type: 'ok' | 'failed', cb: any) => {
let listeners = pubListeners[id]
if (!listeners) return
let idx = listeners[type].indexOf(cb)
if (idx >= 0) listeners[type].splice(idx, 1)
return new Promise((resolve, reject) => {
if (!event.id) {
reject(new Error(`event ${event} has no id`))
return
}
}
let id = event.id
trySend([type, event])
pubListeners[id] = {resolve, reject}
})
}
return {
@@ -349,7 +345,7 @@ export function relayInit(
clearTimeout(timeout)
resolve(events)
})
s.on('event', (event) => {
s.on('event', event => {
events.push(event)
})
}),
@@ -360,7 +356,7 @@ export function relayInit(
s.unsub()
resolve(null)
}, getTimeout)
s.on('event', (event) => {
s.on('event', event => {
s.unsub()
clearTimeout(timeout)
resolve(event)
@@ -379,19 +375,19 @@ export function relayInit(
resolve(event)
})
}),
publish(event): Pub {
return _publishEvent(event, 'EVENT')
async publish(event): Promise<void> {
await _publishEvent(event, 'EVENT')
},
auth(event): Pub {
return _publishEvent(event, 'AUTH')
async auth(event): Promise<void> {
await _publishEvent(event, 'AUTH')
},
connect,
close(): void {
listeners = newListeners()
subListeners = {}
pubListeners = {}
if (ws.readyState === WebSocket.OPEN) {
ws?.close()
if (ws?.readyState === WebSocket.OPEN) {
ws.close()
}
},
get status() {

1346
yarn.lock

File diff suppressed because it is too large Load Diff