Skip to content

Commit 5abc766

Browse files
Merge branch 'main' into fix/csp-ws-http-scheme-mapping
2 parents 6a849db + a76d0b5 commit 5abc766

6 files changed

Lines changed: 60 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
fix: reject expiration timestamp 0 and millisecond-scale values, and accept safe-integer second-based timestamps up to the Postgres int4 max (2038-01-19T03:14:07Z) in getEventExpiration()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
fix: abort in-flight streaming queries when a subscription is cancelled

‎src/handlers/subscribe-message-handler.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { anyPass, equals, isNil, map, omit, propSatisfies, uniqWith } from 'ramda'
2-
// import { addAbortSignal } from 'stream'
2+
import { addAbortSignal } from 'stream'
33
import { pipeline } from 'stream/promises'
44

55
import {
@@ -24,18 +24,18 @@ import { WebSocketAdapterEvent } from '../constants/adapter'
2424
const logger = createLogger('subscribe-message-handler')
2525

2626
export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
27-
//private readonly abortController: AbortController
27+
private readonly abortController: AbortController
2828

2929
public constructor(
3030
private readonly webSocket: IWebSocketAdapter,
3131
private readonly eventRepository: IEventRepository,
3232
private readonly settings: () => Settings,
3333
) {
34-
//this.abortController = new AbortController()
34+
this.abortController = new AbortController()
3535
}
3636

3737
public abort(): void {
38-
//this.abortController.abort()
38+
this.abortController.abort()
3939
}
4040

4141
public async handleMessage(message: SubscribeMessage): Promise<void> {
@@ -90,11 +90,11 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
9090

9191
const findEvents = this.eventRepository.findByFilters(filters).stream()
9292

93-
// const abortableFindEvents = addAbortSignal(this.abortController.signal, findEvents)
93+
const abortableFindEvents = addAbortSignal(this.abortController.signal, findEvents)
9494

9595
try {
9696
await pipeline(
97-
findEvents,
97+
abortableFindEvents,
9898
streamFilter(propSatisfies(isNil, 'deleted_at')),
9999
streamMap(toNostrEvent),
100100
streamFilter(isTagUnexpired),

‎src/utils/event.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ export const isExpiredEvent = (event: Event): boolean => {
245245
return expirationTime <= now
246246
}
247247

248+
// Postgres int4 max (2038-01-19T03:14:07Z). events.expires_at is a signed 32-bit
249+
// integer column — Knex's .unsigned() is a no-op on Postgres — so anything beyond
250+
// this would fail the INSERT and get silently dropped instead of stored.
251+
const MAX_EXPIRATION_TIME = 2147483647
252+
248253
export const getEventExpiration = (event: Event): number | undefined => {
249254
const [, rawExpirationTime] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Expiration) ?? []
250255
if (!rawExpirationTime) {
@@ -253,7 +258,7 @@ export const getEventExpiration = (event: Event): number | undefined => {
253258

254259
const expirationTime = Number(rawExpirationTime)
255260

256-
if (Number.isSafeInteger(expirationTime) && Math.log10(expirationTime) < 10) {
261+
if (Number.isSafeInteger(expirationTime) && expirationTime > 0 && expirationTime <= MAX_EXPIRATION_TIME) {
257262
return expirationTime
258263
}
259264
}

‎test/unit/handlers/subscribe-message-handler.spec.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,19 @@ describe('SubscribeMessageHandler', () => {
357357
await expect(promise).to.eventually.be.rejectedWith(error)
358358
expect(destroySpy).to.have.been.called
359359
})
360+
361+
it('aborts and destroys the event stream when abort() is called', async () => {
362+
isClientSubscribedToEventStub.returns(always(true))
363+
364+
const destroySpy = sandbox.spy(stream, 'destroy')
365+
366+
const promise = (handler as any).fetchAndSend(subscriptionId, filters)
367+
368+
handler.abort()
369+
370+
await expect(promise).to.eventually.be.rejected
371+
expect(destroySpy).to.have.been.called
372+
})
360373
})
361374

362375
describe('.isClientSubscribedToEvent', () => {

‎test/unit/utils/event.spec.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,31 @@ describe('NIP-40', () => {
691691
event.tags = [['expiration', 'a']]
692692
expect(getEventExpiration(event)).to.be.undefined
693693
})
694+
695+
it('returns false if expiration is 0', () => {
696+
event.tags = [['expiration', '0']]
697+
expect(getEventExpiration(event)).to.be.undefined
698+
})
699+
700+
it('returns false if expiration exceeds the Postgres int4 column max', () => {
701+
event.tags = [['expiration', '10000000000']]
702+
expect(getEventExpiration(event)).to.be.undefined
703+
})
704+
705+
it('returns false if expiration is the maximum representable Unix seconds timestamp', () => {
706+
event.tags = [['expiration', '253402300799']]
707+
expect(getEventExpiration(event)).to.be.undefined
708+
})
709+
710+
it('returns true if expiration is the maximum value the expires_at column can store', () => {
711+
event.tags = [['expiration', '2147483647']]
712+
expect(getEventExpiration(event)).to.equal(2147483647)
713+
})
714+
715+
it('returns false if expiration looks like a millisecond-scale timestamp', () => {
716+
event.tags = [['expiration', '1700000000000']]
717+
expect(getEventExpiration(event)).to.be.undefined
718+
})
694719
})
695720

696721
describe('isExpiredEvent', () => {

0 commit comments

Comments
 (0)