diff --git a/nip40.test.ts b/nip40.test.ts new file mode 100644 index 0000000..f484329 --- /dev/null +++ b/nip40.test.ts @@ -0,0 +1,44 @@ +import { describe, test, expect, jest } from 'bun:test' +import { buildEvent } from './test-helpers.ts' +import { getExpiration, isEventExpired, waitForExpire, onExpire } from './nip40.ts' + +describe('getExpiration', () => { + test('returns the expiration as a Date object', () => { + const event = buildEvent({ tags: [['expiration', '123']] }) + const result = getExpiration(event) + expect(result).toEqual(new Date(123000)) + }) +}) + +describe('isEventExpired', () => { + test('returns true when the event has expired', () => { + const event = buildEvent({ tags: [['expiration', '123']] }) + const result = isEventExpired(event) + expect(result).toEqual(true) + }) + + test('returns false when the event has not expired', () => { + const future = Math.floor(Date.now() / 1000) + 10 + const event = buildEvent({ tags: [['expiration', future.toString()]] }) + const result = isEventExpired(event) + expect(result).toEqual(false) + }) +}) + +describe('waitForExpire', () => { + test('returns a promise that resolves when the event expires', async () => { + const event = buildEvent({ tags: [['expiration', '123']] }) + const result = await waitForExpire(event) + expect(result).toEqual(event) + }) +}) + +describe('onExpire', () => { + test('calls the callback when the event expires', async () => { + const event = buildEvent({ tags: [['expiration', '123']] }) + const callback = jest.fn() + onExpire(event, callback) + await new Promise((resolve) => setTimeout(resolve, 200)) + expect(callback).toHaveBeenCalled() + }) +}) diff --git a/nip40.ts b/nip40.ts new file mode 100644 index 0000000..3ce45fe --- /dev/null +++ b/nip40.ts @@ -0,0 +1,49 @@ +import { Event } from './core.ts' + +/** Get the expiration of the event as a `Date` object, if any. */ +function getExpiration(event: Event): Date | undefined { + const tag = event.tags.find(([name]) => name === 'expiration') + if (tag) { + return new Date(parseInt(tag[1]) * 1000) + } +} + +/** Check if the event has expired. */ +function isEventExpired(event: Event): boolean { + const expiration = getExpiration(event) + if (expiration) { + return Date.now() > expiration.getTime() + } else { + return false + } +} + +/** Returns a promise that resolves when the event expires. */ +async function waitForExpire(event: Event): Promise { + const expiration = getExpiration(event) + if (expiration) { + const diff = expiration.getTime() - Date.now() + if (diff > 0) { + await sleep(diff) + return event + } else { + return event + } + } else { + throw new Error('Event has no expiration') + } +} + +/** Calls the callback when the event expires. */ +function onExpire(event: Event, callback: (event: Event) => void): void { + waitForExpire(event) + .then(callback) + .catch(() => {}) +} + +/** Resolves when the given number of milliseconds have elapsed. */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export { getExpiration, isEventExpired, waitForExpire, onExpire }