Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nip77-negentropy-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": minor
---

feat(nip77): add negentropy set-reconciliation core and a sorted event id query
1 change: 1 addition & 0 deletions .knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
],
"ignore": [
".nostr/**",
"src/utils/negentropy.ts",
"src/utils/relay-probe/**"
],
"commitlint": false,
Expand Down
3 changes: 3 additions & 0 deletions src/@types/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface EventPurgeCounts {

export type ExposedPromiseKeys = 'then' | 'catch' | 'finally'

export type ReconciliationItem = Pick<DBEvent, 'event_created_at' | 'event_id'>

export interface IQueryResult<T> extends Pick<Promise<T>, keyof Promise<T> & ExposedPromiseKeys> {
stream(options?: Record<string, any>): PassThrough & AsyncIterable<T>
}
Expand All @@ -42,6 +44,7 @@ export interface IEventRepository {
upsertMany(events: Event[]): Promise<number>
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
countByFilters(filters: SubscriptionFilter[]): Promise<number>
findReconciliationItems(filter: SubscriptionFilter): IQueryResult<ReconciliationItem[]>
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
deleteByPubkeyExceptKinds(pubkey: Pubkey, excludedKinds: number[]): Promise<number>
hasActiveRequestToVanish(pubkey: Pubkey): Promise<boolean>
Expand Down
33 changes: 32 additions & 1 deletion src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import {
} from '../constants/base'
import { DatabaseClient, EventId } from '../@types/base'
import { DBEvent, Event } from '../@types/event'
import { EventPurgeCounts, EventRetentionOptions, IEventRepository, IQueryResult } from '../@types/repositories'
import {
EventPurgeCounts,
EventRetentionOptions,
IEventRepository,
IQueryResult,
ReconciliationItem,
} from '../@types/repositories'
import { toBuffer, toJSON } from '../utils/transform'
import { createLogger } from '../factories/logger-factory'
import { isGenericTagQuery, isGeohashPrefixCriterion, stripGeohashPrefixWildcard } from '../utils/filter'
Expand Down Expand Up @@ -128,6 +134,31 @@ export class EventRepository implements IEventRepository {
return Number(result?.count ?? 0)
}

/**
* Every live event matching the filter as (created_at, id), oldest first, for NIP-77 set
* reconciliation. The filter's limit is ignored on purpose: reconciliation compares whole sets, so
* truncating one side would report the cut-off events as missing. Deleted and expired events are
* excluded because they must never be advertised to a peer.
*/
public findReconciliationItems(filter: SubscriptionFilter): IQueryResult<ReconciliationItem[]> {
logger('querying reconciliation items for %o', filter)

const now = Math.floor(Date.now() / 1000)
const builder = this.readReplicaDbClient<DBEvent>('events').select('events.event_created_at', 'events.event_id')

this.applyFilterConditions(builder, filter)

builder
.whereNull('events.deleted_at')
.andWhere((bd) => {
bd.whereNull('events.expires_at').orWhere('events.expires_at', '>', now)
})
.orderBy('events.event_created_at', 'asc')
.orderBy('events.event_id', 'asc')

return builder as unknown as IQueryResult<ReconciliationItem[]>
}

// Wraps every query so each keeps its own ORDER BY and LIMIT. first.union(rest) would put the
// first query's ORDER BY and LIMIT after the UNION, applying them to the whole result.
private unionFilterQueries(queries: any[]): any {
Expand Down
Loading
Loading