mirror of
https://github.com/Cameri/nostream.git
synced 2025-09-17 19:13:35 +02:00
feat: support parameterized replaceable evts
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable('events', function (table) {
|
||||
table.jsonb('event_deduplication').nullable()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable('events', function (table) {
|
||||
table.dropColumn('event_deduplication')
|
||||
})
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
exports.up = async function (knex) {
|
||||
// NIP-33: Parameterized Replaceable Events
|
||||
|
||||
return knex.schema
|
||||
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
|
||||
.raw(
|
||||
`CREATE UNIQUE INDEX replaceable_events_idx
|
||||
ON events ( event_pubkey, event_kind, event_deduplication )
|
||||
WHERE
|
||||
(
|
||||
event_kind = 0
|
||||
OR event_kind = 3
|
||||
OR (event_kind >= 10000 AND event_kind < 20000)
|
||||
)
|
||||
OR (event_kind >= 30000 AND event_kind < 40000);`,
|
||||
)
|
||||
}
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
|
||||
.raw(
|
||||
'CREATE UNIQUE INDEX replaceable_events_idx ON events ( event_pubkey, event_kind ) WHERE event_kind = 0 OR event_kind = 3 OR (event_kind >= 10000 AND event_kind < 20000);',
|
||||
)
|
||||
}
|
@@ -12,7 +12,8 @@
|
||||
15,
|
||||
16,
|
||||
22,
|
||||
26
|
||||
26,
|
||||
33
|
||||
],
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { EventDelegatorMetadataKey, EventKinds } from '../constants/base'
|
||||
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventKinds } from '../constants/base'
|
||||
import { EventId, Pubkey, Tag } from './base'
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ export interface DelegatedEvent extends Event {
|
||||
[EventDelegatorMetadataKey]?: Pubkey
|
||||
}
|
||||
|
||||
export interface ParameterizedReplaceableEvent extends Event {
|
||||
[EventDeduplicationMetadataKey]: string[]
|
||||
}
|
||||
|
||||
export interface DBEvent {
|
||||
id: string
|
||||
event_id: Buffer
|
||||
@@ -26,6 +30,7 @@ export interface DBEvent {
|
||||
event_tags: Tag[]
|
||||
event_signature: Buffer
|
||||
event_delegator?: Buffer | null
|
||||
event_deduplication?: string | null
|
||||
first_seen: Date
|
||||
deleted_at: Date
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@ export enum EventTags {
|
||||
Pubkey = 'p',
|
||||
// Multicast = 'm',
|
||||
Delegation = 'delegation',
|
||||
Deduplication = 'd',
|
||||
}
|
||||
|
||||
export const EventDelegatorMetadataKey = Symbol('Delegator')
|
||||
|
||||
export const EventDeduplicationMetadataKey = Symbol('Deduplication')
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event'
|
||||
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } from '../utils/event'
|
||||
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
|
||||
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
|
||||
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
|
||||
@@ -7,6 +7,7 @@ import { Factory } from '../@types/base'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { IEventStrategy } from '../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
|
||||
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'
|
||||
|
||||
|
||||
@@ -20,6 +21,8 @@ export const eventStrategyFactory = (
|
||||
return new EphemeralEventStrategy(adapter)
|
||||
} else if (isDeleteEvent(event)) {
|
||||
return new DeleteEventStrategy(adapter, eventRepository)
|
||||
} else if (isParameterizedReplaceableEvent(event)) {
|
||||
return new ParameterizedReplaceableEventStrategy(adapter, eventRepository)
|
||||
}
|
||||
|
||||
return new DefaultEventStrategy(adapter, eventRepository)
|
||||
|
@@ -0,0 +1,34 @@
|
||||
import { Event, ParameterizedReplaceableEvent } from '../../@types/event'
|
||||
import { EventDeduplicationMetadataKey, EventTags } from '../../constants/base'
|
||||
import { createLogger } from '../../factories/logger-factory'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
import { WebSocketAdapterEvent } from '../../constants/adapter'
|
||||
|
||||
const debug = createLogger('parameterized-replaceable-event-strategy')
|
||||
|
||||
export class ParameterizedReplaceableEventStrategy implements IEventStrategy<Event, Promise<void>> {
|
||||
public constructor(
|
||||
private readonly webSocket: IWebSocketAdapter,
|
||||
private readonly eventRepository: IEventRepository,
|
||||
) { }
|
||||
|
||||
public async execute(event: Event): Promise<void> {
|
||||
debug('received event: %o', event)
|
||||
|
||||
const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [null, '']
|
||||
|
||||
const parameterizedReplaceableEvent: ParameterizedReplaceableEvent = {
|
||||
...event,
|
||||
[EventDeduplicationMetadataKey]: deduplication,
|
||||
}
|
||||
|
||||
const count = await this.eventRepository.upsert(parameterizedReplaceableEvent)
|
||||
if (!count) {
|
||||
return
|
||||
}
|
||||
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ import {
|
||||
modulo,
|
||||
nth,
|
||||
omit,
|
||||
paths,
|
||||
pipe,
|
||||
prop,
|
||||
propSatisfies,
|
||||
@@ -28,10 +29,10 @@ import {
|
||||
|
||||
import { DatabaseClient, EventId } from '../@types/base'
|
||||
import { DBEvent, Event } from '../@types/event'
|
||||
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey } from '../constants/base'
|
||||
import { IEventRepository, IQueryResult } from '../@types/repositories'
|
||||
import { toBuffer, toJSON } from '../utils/transform'
|
||||
import { createLogger } from '../factories/logger-factory'
|
||||
import { EventDelegatorMetadataKey } from '../constants/base'
|
||||
import { isGenericTagQuery } from '../utils/filter'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
|
||||
@@ -157,11 +158,11 @@ export class EventRepository implements IEventRepository {
|
||||
}
|
||||
|
||||
public async create(event: Event): Promise<number> {
|
||||
debug('creating event: %o', event)
|
||||
return this.insert(event).then(prop('rowCount') as () => number)
|
||||
}
|
||||
|
||||
private insert(event: Event) {
|
||||
debug('inserting event: %o', event)
|
||||
const row = applySpec({
|
||||
event_id: pipe(prop('id'), toBuffer),
|
||||
event_pubkey: pipe(prop('pubkey'), toBuffer),
|
||||
@@ -186,6 +187,7 @@ export class EventRepository implements IEventRepository {
|
||||
|
||||
public upsert(event: Event): Promise<number> {
|
||||
debug('upserting event: %o', event)
|
||||
|
||||
const toJSON = (input: any) => JSON.stringify(input)
|
||||
|
||||
const row = applySpec({
|
||||
@@ -201,13 +203,23 @@ export class EventRepository implements IEventRepository {
|
||||
pipe(prop(EventDelegatorMetadataKey as any), toBuffer),
|
||||
always(null),
|
||||
),
|
||||
event_deduplication: ifElse(
|
||||
propSatisfies(isNil, EventDeduplicationMetadataKey),
|
||||
pipe(paths([['pubkey'], ['kind']]), toJSON),
|
||||
pipe(prop(EventDeduplicationMetadataKey as any), toJSON),
|
||||
),
|
||||
})(event)
|
||||
|
||||
const query = this.dbClient('events')
|
||||
.insert(row)
|
||||
// NIP-16: Replaceable Events
|
||||
.onConflict(this.dbClient.raw('(event_pubkey, event_kind) WHERE event_kind = 0 OR event_kind = 3 OR event_kind >= 10000 AND event_kind < 2000'))
|
||||
.merge(omit(['event_pubkey', 'event_kind'])(row))
|
||||
// NIP-33: Parameterized Replaceable Events
|
||||
.onConflict(
|
||||
this.dbClient.raw(
|
||||
'(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)'
|
||||
)
|
||||
)
|
||||
.merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row))
|
||||
.where('events.event_created_at', '<', row.event_created_at)
|
||||
|
||||
const promise = query.then(prop('rowCount') as () => number)
|
||||
|
@@ -174,6 +174,10 @@ export const isEphemeralEvent = (event: Event): boolean => {
|
||||
return event.kind >= 20000 && event.kind < 30000
|
||||
}
|
||||
|
||||
export const isParameterizedReplaceableEvent = (event: Event): boolean => {
|
||||
return event.kind >= 30000 && event.kind < 40000
|
||||
}
|
||||
|
||||
export const isDeleteEvent = (event: Event): boolean => {
|
||||
return event.kind === EventKinds.DELETE
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
isEventIdValid,
|
||||
isEventMatchingFilter,
|
||||
isEventSignatureValid,
|
||||
isParameterizedReplaceableEvent,
|
||||
isReplaceableEvent,
|
||||
serializeEvent,
|
||||
} from '../../../src/utils/event'
|
||||
@@ -475,3 +476,15 @@ describe('NIP-09', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-33', () => {
|
||||
describe('isParameterizedReplaceableEvent', () => {
|
||||
it('returns true if event is a parameterized replaceable event', () => {
|
||||
expect(isParameterizedReplaceableEvent({ kind: 30000 } as any)).to.be.true
|
||||
})
|
||||
|
||||
it('returns false if event is a parameterized replaceable event', () => {
|
||||
expect(isParameterizedReplaceableEvent({ kind: 40000 } as any)).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Reference in New Issue
Block a user