mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
nip77: adds wrapper for negentropy and fallback for yieldThread MessageChannel
This commit is contained in:
@@ -331,13 +331,23 @@ export class AbstractRelay {
|
|||||||
so.close(data[2] as string)
|
so.close(data[2] as string)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case 'NOTICE':
|
case 'NOTICE': {
|
||||||
this.onnotice(data[1] as string)
|
this.onnotice(data[1] as string)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
case 'AUTH': {
|
case 'AUTH': {
|
||||||
this.challenge = data[1] as string
|
this.challenge = data[1] as string
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case 'NEG-ERR':
|
||||||
|
case 'NEG-MSG': {
|
||||||
|
const msg = data[2] as string
|
||||||
|
const id = data[1] as string
|
||||||
|
const so = this.openSubs.get(id)
|
||||||
|
if (!so || !so.onnegentropy) return
|
||||||
|
so.onnegentropy(msg, data[0] === 'NEG-ERR')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return
|
return
|
||||||
@@ -404,9 +414,12 @@ export class AbstractRelay {
|
|||||||
public subscribe(
|
public subscribe(
|
||||||
filters: Filter[],
|
filters: Filter[],
|
||||||
params: Partial<SubscriptionParams> & { label?: string; id?: string },
|
params: Partial<SubscriptionParams> & { label?: string; id?: string },
|
||||||
|
fireImmediately: boolean = true,
|
||||||
): Subscription {
|
): Subscription {
|
||||||
const subscription = this.prepareSubscription(filters, params)
|
const subscription = this.prepareSubscription(filters, params)
|
||||||
|
if (fireImmediately) {
|
||||||
subscription.fire()
|
subscription.fire()
|
||||||
|
}
|
||||||
return subscription
|
return subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +472,7 @@ export class Subscription {
|
|||||||
public alreadyHaveEvent: ((id: string) => boolean) | undefined
|
public alreadyHaveEvent: ((id: string) => boolean) | undefined
|
||||||
public receivedEvent: ((relay: AbstractRelay, id: string) => void) | undefined
|
public receivedEvent: ((relay: AbstractRelay, id: string) => void) | undefined
|
||||||
|
|
||||||
|
public onnegentropy: ((msg: string, isError: boolean) => void) | undefined
|
||||||
public onevent: (evt: Event) => void
|
public onevent: (evt: Event) => void
|
||||||
public oneose: (() => void) | undefined
|
public oneose: (() => void) | undefined
|
||||||
public onclose: ((reason: string) => void) | undefined
|
public onclose: ((reason: string) => void) | undefined
|
||||||
@@ -474,6 +488,7 @@ export class Subscription {
|
|||||||
this.receivedEvent = params.receivedEvent
|
this.receivedEvent = params.receivedEvent
|
||||||
this.eoseTimeout = params.eoseTimeout || relay.baseEoseTimeout
|
this.eoseTimeout = params.eoseTimeout || relay.baseEoseTimeout
|
||||||
|
|
||||||
|
this.onnegentropy = params.onnegentropy
|
||||||
this.oneose = params.oneose
|
this.oneose = params.oneose
|
||||||
this.onclose = params.onclose
|
this.onclose = params.onclose
|
||||||
this.onevent =
|
this.onevent =
|
||||||
@@ -523,6 +538,7 @@ export class Subscription {
|
|||||||
export type SubscriptionParams = {
|
export type SubscriptionParams = {
|
||||||
onevent?: (evt: Event) => void
|
onevent?: (evt: Event) => void
|
||||||
oneose?: () => void
|
oneose?: () => void
|
||||||
|
onnegentropy?: (msg: string, isError: boolean) => void
|
||||||
onclose?: (reason: string) => void
|
onclose?: (reason: string) => void
|
||||||
alreadyHaveEvent?: (id: string) => boolean
|
alreadyHaveEvent?: (id: string) => boolean
|
||||||
receivedEvent?: (relay: AbstractRelay, id: string) => void
|
receivedEvent?: (relay: AbstractRelay, id: string) => void
|
||||||
|
|||||||
19
helpers.ts
19
helpers.ts
@@ -1,7 +1,10 @@
|
|||||||
import { verifiedSymbol, type Event, type Nostr, VerifiedEvent } from './core.ts'
|
import { verifiedSymbol, type Event, type Nostr, VerifiedEvent } from './core.ts'
|
||||||
|
|
||||||
export async function yieldThread() {
|
export async function yieldThread() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Check if MessageChannel is available
|
||||||
|
if (typeof MessageChannel !== 'undefined') {
|
||||||
const ch = new MessageChannel()
|
const ch = new MessageChannel()
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
// @ts-ignore (typescript thinks this property should be called `removeListener`, but in fact it's `removeEventListener`)
|
// @ts-ignore (typescript thinks this property should be called `removeListener`, but in fact it's `removeEventListener`)
|
||||||
@@ -12,6 +15,20 @@ export async function yieldThread() {
|
|||||||
ch.port1.addEventListener('message', handler)
|
ch.port1.addEventListener('message', handler)
|
||||||
ch.port2.postMessage(0)
|
ch.port2.postMessage(0)
|
||||||
ch.port1.start()
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
index.ts
1
index.ts
@@ -24,6 +24,7 @@ export * as nip47 from './nip47.ts'
|
|||||||
export * as nip54 from './nip54.ts'
|
export * as nip54 from './nip54.ts'
|
||||||
export * as nip57 from './nip57.ts'
|
export * as nip57 from './nip57.ts'
|
||||||
export * as nip59 from './nip59.ts'
|
export * as nip59 from './nip59.ts'
|
||||||
|
export * as nip77 from './nip77.ts'
|
||||||
export * as nip98 from './nip98.ts'
|
export * as nip98 from './nip98.ts'
|
||||||
|
|
||||||
export * as kinds from './kinds.ts'
|
export * as kinds from './kinds.ts'
|
||||||
|
|||||||
91
nip77.test.ts
Normal file
91
nip77.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect } from 'bun:test'
|
||||||
|
|
||||||
|
import { sendNegentropyMessage, openNegentropyWithMessage, closeNegentropy } from './nip77.ts'
|
||||||
|
import type { Filter } from './filter.ts'
|
||||||
|
import { AbstractRelay } from './abstract-relay.ts'
|
||||||
|
import type { Event, VerifiedEvent } from './core.ts'
|
||||||
|
|
||||||
|
// A minimal mock relay implementing send(). We don't need websocket behavior here.
|
||||||
|
class MockRelay extends AbstractRelay {
|
||||||
|
public sent: string[] = []
|
||||||
|
constructor(url: string = 'wss://example.com') {
|
||||||
|
// pass a dummy verifyEvent function to satisfy constructor (always returns true)
|
||||||
|
const verifyEvent = (event: Event): event is VerifiedEvent => true
|
||||||
|
super(url, { verifyEvent })
|
||||||
|
// Pretend it's connected so send() doesn't throw.
|
||||||
|
// @ts-ignore accessing private
|
||||||
|
this.connectionPromise = Promise.resolve()
|
||||||
|
}
|
||||||
|
public override async send(message: string) {
|
||||||
|
this.sent.push(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJSON(message: string) {
|
||||||
|
return JSON.parse(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('nip77 negentropy message helpers', () => {
|
||||||
|
it('sendNegentropyMessage should send NEG-MSG with generated subscription id and filters flattened', () => {
|
||||||
|
const relay = new MockRelay()
|
||||||
|
const filters: Filter[] = [
|
||||||
|
{ kinds: [1], authors: ['abc'] },
|
||||||
|
{ ids: ['deadbeef'] },
|
||||||
|
]
|
||||||
|
sendNegentropyMessage(relay as any, 'hello', filters)
|
||||||
|
|
||||||
|
expect(relay.sent.length).toBe(1)
|
||||||
|
const arr = extractJSON(relay.sent[0])
|
||||||
|
expect(arr[0]).toBe('NEG-MSG')
|
||||||
|
expect(typeof arr[1]).toBe('string') // auto sub id
|
||||||
|
// message should include each filter object fields flattened after the sub id
|
||||||
|
// Request format built in nip77.ts: ["NEG-MSG","subId",<filters without outer []>,"msg"]
|
||||||
|
// So positions 2..n-2 are filter objects; last element is the msg string
|
||||||
|
expect(arr[arr.length - 1]).toBe('hello')
|
||||||
|
// Ensure at least one property from each filter is present
|
||||||
|
const serialized = relay.sent[0]
|
||||||
|
expect(serialized.includes('"kinds":[1]')).toBe(true)
|
||||||
|
expect(serialized.includes('"authors":["abc"]')).toBe(true)
|
||||||
|
expect(serialized.includes('"ids":["deadbeef"]')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('openNegentropyWithMessage should send NEG-OPEN', () => {
|
||||||
|
const relay = new MockRelay()
|
||||||
|
const filters: Filter[] = [{ kinds: [3] }]
|
||||||
|
openNegentropyWithMessage(relay as any, 'init', filters, 'sub123')
|
||||||
|
|
||||||
|
expect(relay.sent.length).toBe(1)
|
||||||
|
const arr = extractJSON(relay.sent[0])
|
||||||
|
expect(arr[0]).toBe('NEG-OPEN')
|
||||||
|
expect(arr[1]).toBe('sub123')
|
||||||
|
expect(arr[arr.length - 1]).toBe('init')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closeNegentropy should send NEG-CLOSE with given subscription id', () => {
|
||||||
|
const relay = new MockRelay()
|
||||||
|
closeNegentropy(relay as any, 'subXYZ')
|
||||||
|
|
||||||
|
expect(relay.sent.length).toBe(1)
|
||||||
|
const arr = extractJSON(relay.sent[0])
|
||||||
|
expect(arr).toEqual(['NEG-CLOSE', 'subXYZ'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sendNegentropyMessage should honor provided subscriptionId', () => {
|
||||||
|
const relay = new MockRelay()
|
||||||
|
const filters: Filter[] = [{ kinds: [1] }]
|
||||||
|
sendNegentropyMessage(relay as any, 'custom', filters, 'customSub')
|
||||||
|
const arr = extractJSON(relay.sent[0])
|
||||||
|
expect(arr[1]).toBe('customSub')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty filters array (request still valid)', () => {
|
||||||
|
const relay = new MockRelay()
|
||||||
|
sendNegentropyMessage(relay as any, 'nofilters', [])
|
||||||
|
const arr = extractJSON(relay.sent[0])
|
||||||
|
expect(arr[0]).toBe('NEG-MSG')
|
||||||
|
expect(typeof arr[1]).toBe('string')
|
||||||
|
// With empty filters array we expect exactly 3 elements: type, subId, msg
|
||||||
|
expect(arr.length).toBe(3)
|
||||||
|
expect(arr[2]).toBe('nofilters')
|
||||||
|
})
|
||||||
|
})
|
||||||
21
nip77.ts
Normal file
21
nip77.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Filter } from './filter.ts'
|
||||||
|
import { AbstractRelay } from './relay.ts'
|
||||||
|
|
||||||
|
function sendNegentropyToRelay(open: boolean, relay: AbstractRelay, msg: string, filters: Filter[], subscriptionId?: any): void {
|
||||||
|
const subId = subscriptionId || Math.random().toString(36).slice(2, 10)
|
||||||
|
const parts: any[] = [open ? 'NEG-OPEN' : 'NEG-MSG', subId, ...filters, msg]
|
||||||
|
relay.send(JSON.stringify(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendNegentropyMessage(relay: AbstractRelay, msg: string, filters: Filter[], subscriptionId?: any): void {
|
||||||
|
sendNegentropyToRelay(false, relay, msg, filters, subscriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openNegentropyWithMessage(relay: AbstractRelay, msg: string, filters: Filter[], subscriptionId?: any): void {
|
||||||
|
sendNegentropyToRelay(true, relay, msg, filters, subscriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeNegentropy(relay: AbstractRelay, subscriptionId: string): void {
|
||||||
|
const request = '["NEG-CLOSE","' + subscriptionId + '"]'
|
||||||
|
relay.send(request)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user