Subscription Management
NDK attempts to intelligently group subscriptions to avoid excessively hitting relays with too many subscriptions when similar requests are going to be created with similar requests.
Take the example of an application rendering a list of events along with the authors' name.
This would typically be accomplished by creating a subscription for the desired events, say, kind:1s with a #nostr
tag.
const sub = ndk.subscribe({ kinds: [1], "#t": ["nostr"] });
sub.on("event", (event: NDKEvent) => {
const author = event.author;
const profile = await author.fetchProfile();
console.log(`${profile.name}: ${event.content}`);
});
Now, this seemingly simple approach would have created a kind:0 subscription (fetchProfile()
) for each note.
Not great. Most relays will start to reject subscriptions when you have around 10 or 20 active requests.
In this case, NDK will automatically realize that you are requesting kind:0
events for a lot of different pubkeys and group them into a single subscription.
Without grouping:
[ "REQ", "<sub1>", '{ "kinds": [0], pubkeys: [ "pubkey1" ] }'],
[ "REQ", "<sub2>", '{ "kinds": [0], pubkeys: [ "pubkey2" ] }'],
[ "REQ", "<sub3>", '{ "kinds": [0], pubkeys: [ "pubkey3" ] }'],
[ "REQ", "<sub4>", '{ "kinds": [0], pubkeys: [ "pubkey4" ] }'],
[ "REQ", "<sub5>", '{ "kinds": [0], pubkeys: [ "pubkey5" ] }'],
With grouping:
[ "REQ", "<sub1>", '{ "kinds": [0], pubkeys: [ "pubkey1", "pubkey2", "pubkey3", "pubkey4", "pubkey5" ] }'],
Application code doesn't need to concern itself with checking if the event they are receiving is the one they asked for; NDK will only call the event handler with the correct event so that the grouping is transparent to the application.
Disabling Grouping
Sometimes you have a specific need or are certain that you won't be requesting multiple requests of the same type, so we can safely disable grouping and enjoy a small performance boost (since we don't need to wait for grouping to happen).
const sub = ndk.subscribe({ kinds: [1], "#t": ["nostr"] }, { groupable: false });
This will make the REQ for kind:1
events to hit the relays immediately and skip the 100ms
(default) grouping window.
If you want to change the grouping delay you can do so by setting the groupingDelay
option
const sub = ndk.subscribe({ kinds: [1], "#t": ["nostr"] }, { groupingDelay: 500 });
You can also establish how the delay should be interpreted:
const sub = ndk.subscribe({ kinds: [1], "#t": ["nostr"] }, { groupingDelayType: "at-least" });
// * "at-least" means "wait at least this long before sending the subscription"
// * "at-most" means "wait at most this long before sending the subscription"
When using at-least
the subscription timer will be reset every time a new subscription is added to the group.
For example, if you create two subscriptions, one at t=0
and the other one 50ms later (t=50ms
), with a groupableDelay
of 200ms
, at-least
would send the subscription at t=250ms
and at-most
would send it at t=200ms
.
Deferred subscription
Another useful interface to creating subscriptions is to pass event handlers within the subscription itself. In this way, the subscription will first connect the event handlers and then auto-start the subscription.
const sub = ndk.subscribe(
{ kinds: [1] }, // filters
{ groupable: false }, /// subscription options
relaySet, // optional relaySet
{
onEvent: (event: NDKEvent) => console.log("an event was received", event.id),
onEose: () => console.log("the subscription EOSED"),
}
);
Advanced uses
cacheUnconstrainFilter
NDKSubscribe
supports passing a number of filters that will be removed when querying the cache; this allows you to pass certain filters when going to relays and drop them when going to the cache. For example, a typical use is querying for events since
a certain timestamp. But you might want to load, in the same subscription everything that the cache also has, regardless of that timestamp.
const events = ndk.subscribe([{ kinds: [1], limit: 10 }, { cacheUnconstrainFilter: ["limit"] }]);
This query will hit relays and only load 10 events from each relay that it hits, but the cache will be unconstrained from the limit
filter and everything that matches the kinds:[1]
filter will be loaded by the subscription.