fix: avoid updating cache if event is old

This commit is contained in:
Shusui MOYATANI
2023-10-02 01:05:34 +09:00
parent 326acacfc8
commit ab171c0016
6 changed files with 174 additions and 19 deletions

View File

@@ -0,0 +1,132 @@
import assert from 'assert';
import { Event as NostrEvent } from 'nostr-tools';
import { describe, it } from 'vitest';
import { compareEvents, pickLatestEvent } from '@/nostr/event/comparator';
describe('compareEvents', () => {
it('should return negative value if first event was made earlier than second one', () => {
const result = compareEvents(
{
id: 'b39d39a256ea0824436f1604830030c78f8a15c42b97036fe68124edc3452cc5',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169670,
kind: 1,
tags: [],
content: 'hello',
sig: '90787036c2cdb31125b7b6ef0ca746458d68e0a1d151243a7f29c7272e7be7ec14a72e425e2cf1cb0d9552e3d5acf97d20d4445ba71a710c5354153eeb9848ae',
},
{
id: 'ec6a3a0a1061a45502b66df8fa4f3454a32fe11a6ce79bfc4b2affafa0346da9',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169675,
kind: 1,
tags: [],
content: 'hello',
sig: '65ac58f314c99274304c540221e9ff603c26472f91cbb59bf5203e397a686917bb777c6e4c7ac43b27b0b519fc490e91b383d22b9ee8f1efc88175b2c6108c79',
},
);
assert(result < 0);
});
it('should return positive number if first event was made later than second one', () => {
const result = compareEvents(
{
id: 'ec6a3a0a1061a45502b66df8fa4f3454a32fe11a6ce79bfc4b2affafa0346da9',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169675,
kind: 1,
tags: [],
content: 'hello',
sig: '65ac58f314c99274304c540221e9ff603c26472f91cbb59bf5203e397a686917bb777c6e4c7ac43b27b0b519fc490e91b383d22b9ee8f1efc88175b2c6108c79',
},
{
id: 'b39d39a256ea0824436f1604830030c78f8a15c42b97036fe68124edc3452cc5',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169670,
kind: 1,
tags: [],
content: 'hello',
sig: '90787036c2cdb31125b7b6ef0ca746458d68e0a1d151243a7f29c7272e7be7ec14a72e425e2cf1cb0d9552e3d5acf97d20d4445ba71a710c5354153eeb9848ae',
},
);
assert(result > 0);
});
it('should return negative number if both are made at the same time and first id is smaller than second one', () => {
const result = compareEvents(
{
id: '2b4f598e0586b8e54c7fecfd2a1686ef7d91f0e55236bf53d437b100a29c07ea',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169680,
kind: 1,
tags: [],
content: 'hello',
sig: 'a6d20521d9c141852d1a85bd139b13f26457d0cb878909fa92357e8d47b9bdfa3199fdb7c5a1cf1c1f9e38a0450e453577ae8325b5036fdf75ecfa7077e775d9',
},
{
id: '945a7a4df86e69eeafaba5b4025acb175f67e56a58ad3b8c8ce6532e6aad49a8',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169680,
kind: 1,
tags: [],
content: 'hello world',
sig: 'ca57fbea0f6a0e6a9a93920c204d4b829d0dcc71f0cff9b5e7d91dfe1af2245178d88c34acf9ff72d1afe2d799ef3f4b51a37a8ad2e8a27a9859e58fe984f6ee',
},
);
assert(result < 0);
});
});
describe('pickLatestEvent', () => {
it('should return the first event if there is only a single event', () => {
const events: NostrEvent[] = [
{
id: '2b4f598e0586b8e54c7fecfd2a1686ef7d91f0e55236bf53d437b100a29c07ea',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169680,
kind: 1,
tags: [],
content: 'hello',
sig: 'a6d20521d9c141852d1a85bd139b13f26457d0cb878909fa92357e8d47b9bdfa3199fdb7c5a1cf1c1f9e38a0450e453577ae8325b5036fdf75ecfa7077e775d9',
},
];
const result = pickLatestEvent(events);
assert.deepStrictEqual(result, events[0]);
});
it('should return latest event', () => {
const events: NostrEvent[] = [
{
id: 'b39d39a256ea0824436f1604830030c78f8a15c42b97036fe68124edc3452cc5',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169670,
kind: 1,
tags: [],
content: 'hello',
sig: '90787036c2cdb31125b7b6ef0ca746458d68e0a1d151243a7f29c7272e7be7ec14a72e425e2cf1cb0d9552e3d5acf97d20d4445ba71a710c5354153eeb9848ae',
},
{
id: 'ec6a3a0a1061a45502b66df8fa4f3454a32fe11a6ce79bfc4b2affafa0346da9',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169675,
kind: 1,
tags: [],
content: 'hello',
sig: '65ac58f314c99274304c540221e9ff603c26472f91cbb59bf5203e397a686917bb777c6e4c7ac43b27b0b519fc490e91b383d22b9ee8f1efc88175b2c6108c79',
},
{
id: '2b4f598e0586b8e54c7fecfd2a1686ef7d91f0e55236bf53d437b100a29c07ea',
pubkey: '6e62e578bdf608e250e93c25dc0cbadbda8db17e6fc3a28cdce8a2f56db7d106',
created_at: 1696169680,
kind: 1,
tags: [],
content: 'hello',
sig: 'a6d20521d9c141852d1a85bd139b13f26457d0cb878909fa92357e8d47b9bdfa3199fdb7c5a1cf1c1f9e38a0450e453577ae8325b5036fdf75ecfa7077e775d9',
},
];
const result = pickLatestEvent(events);
assert.deepStrictEqual(result, events[2]);
});
});

View File

@@ -0,0 +1,22 @@
import { Event as NostrEvent } from 'nostr-tools';
/**
* compareEvents compares events by created_at and id.
*
* Comparison by id is defined in NIP-01 for parameterized replaceable events
* but it is used here to ensure consistent results for sorting.
*/
export const compareEvents = (a: NostrEvent, b: NostrEvent): number => {
const diff = a.created_at - b.created_at;
if (diff !== 0) return diff;
return a.id > b.id ? 1 : -1;
};
export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | undefined => {
if (events.length === 0) return undefined;
if (events.length === 1) return events[0];
return events.reduce((a, b) => (compareEvents(a, b) > 0 ? a : b));
};
export const sortEvents = (events: NostrEvent[]) =>
Array.from(events).sort((a, b) => -compareEvents(a, b));

View File

@@ -1,7 +1,9 @@
import { QueryClient, QueryKey } from '@tanstack/solid-query'; import { QueryClient, QueryKey } from '@tanstack/solid-query';
import { uniqBy } from 'lodash';
import { Event as NostrEvent } from 'nostr-tools'; import { Event as NostrEvent } from 'nostr-tools';
import { BatchedEventsTask, pickLatestEvent, registerTask } from '@/nostr/useBatchedEvents'; import { pickLatestEvent, sortEvents } from '@/nostr/event/comparator';
import { BatchedEventsTask, registerTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout'; import timeout from '@/utils/timeout';
export const latestEventQuery = export const latestEventQuery =
@@ -20,7 +22,11 @@ export const latestEventQuery =
}); });
task.onUpdate((events) => { task.onUpdate((events) => {
const latest = pickLatestEvent(events); const latest = pickLatestEvent(events);
queryClient.setQueryData(queryKey, latest); queryClient.setQueryData(queryKey, (prev: NostrEvent | undefined) =>
prev == null || (latest != null && latest.created_at > prev.created_at)
? latest
: undefined,
);
}); });
registerTask({ task, signal }); registerTask({ task, signal });
return timeout(15000, `${JSON.stringify(queryKey)}`)(promise); return timeout(15000, `${JSON.stringify(queryKey)}`)(promise);
@@ -39,7 +45,12 @@ export const eventsQuery =
if (task == null) return Promise.resolve([]); if (task == null) return Promise.resolve([]);
const promise = task.toUpdatePromise().catch(() => []); const promise = task.toUpdatePromise().catch(() => []);
task.onUpdate((events) => { task.onUpdate((events) => {
queryClient.setQueryData(queryKey, events); // TODO consider kind:5 deletion
queryClient.setQueryData(queryKey, (prev: NostrEvent[] | undefined) => {
if (prev == null) return events;
const deduped = uniqBy([...prev, ...events], (e) => e.id);
return sortEvents(deduped);
});
}); });
registerTask({ task, signal }); registerTask({ task, signal });
return timeout(15000, `${JSON.stringify(queryKey)}`)(promise); return timeout(15000, `${JSON.stringify(queryKey)}`)(promise);

View File

@@ -1,7 +1,8 @@
import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools'; import { type Event as NostrEvent, type Filter, Kind, utils } from 'nostr-tools';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import { genericEvent } from '@/nostr/event'; import { genericEvent } from '@/nostr/event';
import { pickLatestEvent } from '@/nostr/event/comparator';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';
import useStats from '@/nostr/useStats'; import useStats from '@/nostr/useStats';
import ObservableTask from '@/utils/batch/ObservableTask'; import ObservableTask from '@/utils/batch/ObservableTask';
@@ -29,19 +30,9 @@ type TaskArg =
| RepostsTask | RepostsTask
| ParameterizedReplaceableEventTask; | ParameterizedReplaceableEventTask;
export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | null => {
if (events.length === 0) return null;
return events.reduce((a, b) => {
const diff = a.created_at - b.created_at;
if (diff > 0) return a;
if (diff < 0) return b;
return a.id < b.id ? a : b;
});
};
export class BatchedEventsTask extends ObservableTask<TaskArg, NostrEvent[]> { export class BatchedEventsTask extends ObservableTask<TaskArg, NostrEvent[]> {
addEvent(event: NostrEvent) { addEvent(event: NostrEvent) {
this.updateWith((current) => [...(current ?? []), event]); this.updateWith((current) => utils.insertEventIntoDescendingList(current ?? [], event));
} }
firstEventPromise(): Promise<NostrEvent> { firstEventPromise(): Promise<NostrEvent> {

View File

@@ -3,7 +3,8 @@ import { createMemo, observable } from 'solid-js';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query'; import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools'; import { Event as NostrEvent } from 'nostr-tools';
import { registerTask, BatchedEventsTask, pickLatestEvent } from '@/nostr/useBatchedEvents'; import { pickLatestEvent } from '@/nostr/event/comparator';
import { registerTask, BatchedEventsTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout'; import timeout from '@/utils/timeout';
// Parameterized Replaceable Event // Parameterized Replaceable Event

View File

@@ -4,6 +4,7 @@ import uniqBy from 'lodash/uniqBy';
import { utils } from 'nostr-tools'; import { utils } from 'nostr-tools';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import { sortEvents } from '@/nostr/event/comparator';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';
import useStats from '@/nostr/useStats'; import useStats from '@/nostr/useStats';
@@ -29,9 +30,6 @@ export type UseSubscriptionProps = {
debugId?: string; debugId?: string;
}; };
const sortEvents = (events: NostrEvent[]) =>
Array.from(events).sort((a, b) => b.created_at - a.created_at);
let count = 0; let count = 0;
const { setActiveSubscriptions } = useStats(); const { setActiveSubscriptions } = useStats();