Skip to content

Commit 4cf419f

Browse files
feat(nip77): add negentropy reconciliation core (#809)
* feat(nip77): add negentropy set reconciliation core Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * feat(nip77): add repository query for reconciliation items Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * test(nip77): cover negentropy core and reconciliation query Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * fix(nip77): address review findings in negentropy core Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> --------- Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> Co-authored-by: Ricardo Cabral <me@ricardocabral.io>
1 parent c81407d commit 4cf419f

7 files changed

Lines changed: 1151 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
feat(nip77): add negentropy set-reconciliation core and a sorted event id query

‎.knip.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
],
1616
"ignore": [
1717
".nostr/**",
18+
"src/utils/negentropy.ts",
1819
"src/utils/relay-probe/**"
1920
],
2021
"commitlint": false,

‎src/@types/repositories.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface EventPurgeCounts {
3131

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

34+
export type ReconciliationItem = Pick<DBEvent, 'event_created_at' | 'event_id'>
35+
3436
export interface IQueryResult<T> extends Pick<Promise<T>, keyof Promise<T> & ExposedPromiseKeys> {
3537
stream(options?: Record<string, any>): PassThrough & AsyncIterable<T>
3638
}
@@ -42,6 +44,7 @@ export interface IEventRepository {
4244
upsertMany(events: Event[]): Promise<number>
4345
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
4446
countByFilters(filters: SubscriptionFilter[]): Promise<number>
47+
findReconciliationItems(filter: SubscriptionFilter): IQueryResult<ReconciliationItem[]>
4548
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
4649
deleteByPubkeyExceptKinds(pubkey: Pubkey, excludedKinds: number[]): Promise<number>
4750
hasActiveRequestToVanish(pubkey: Pubkey): Promise<boolean>

‎src/repositories/event-repository.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import {
2323
} from '../constants/base'
2424
import { DatabaseClient, EventId } from '../@types/base'
2525
import { DBEvent, Event } from '../@types/event'
26-
import { EventPurgeCounts, EventRetentionOptions, IEventRepository, IQueryResult } from '../@types/repositories'
26+
import {
27+
EventPurgeCounts,
28+
EventRetentionOptions,
29+
IEventRepository,
30+
IQueryResult,
31+
ReconciliationItem,
32+
} from '../@types/repositories'
2733
import { toBuffer, toJSON } from '../utils/transform'
2834
import { createLogger } from '../factories/logger-factory'
2935
import { isGenericTagQuery, isGeohashPrefixCriterion, stripGeohashPrefixWildcard } from '../utils/filter'
@@ -128,6 +134,31 @@ export class EventRepository implements IEventRepository {
128134
return Number(result?.count ?? 0)
129135
}
130136

137+
/**
138+
* Every live event matching the filter as (created_at, id), oldest first, for NIP-77 set
139+
* reconciliation. The filter's limit is ignored on purpose: reconciliation compares whole sets, so
140+
* truncating one side would report the cut-off events as missing. Deleted and expired events are
141+
* excluded because they must never be advertised to a peer.
142+
*/
143+
public findReconciliationItems(filter: SubscriptionFilter): IQueryResult<ReconciliationItem[]> {
144+
logger('querying reconciliation items for %o', filter)
145+
146+
const now = Math.floor(Date.now() / 1000)
147+
const builder = this.readReplicaDbClient<DBEvent>('events').select('events.event_created_at', 'events.event_id')
148+
149+
this.applyFilterConditions(builder, filter)
150+
151+
builder
152+
.whereNull('events.deleted_at')
153+
.andWhere((bd) => {
154+
bd.whereNull('events.expires_at').orWhere('events.expires_at', '>', now)
155+
})
156+
.orderBy('events.event_created_at', 'asc')
157+
.orderBy('events.event_id', 'asc')
158+
159+
return builder as unknown as IQueryResult<ReconciliationItem[]>
160+
}
161+
131162
// Wraps every query so each keeps its own ORDER BY and LIMIT. first.union(rest) would put the
132163
// first query's ORDER BY and LIMIT after the UNION, applying them to the whole result.
133164
private unionFilterQueries(queries: any[]): any {

0 commit comments

Comments
 (0)