GetStream / stream-chat-js

JS / Browser Client - Build Chat with GetStream.io
https://getstream.io/chat/
Other
183 stars 76 forks source link

perf: targeted events #1385

Open arnautov-anton opened 1 month ago

arnautov-anton commented 1 month ago

The Why

Targeted events are a runtime extension of event types - by default we provide a limited set of types which we can hook onto but say you have 1000 instances of a Thread BFF which have and manage their own state by hooking onto these events. Each of these instances registers around 10 event listeners but incoming events are usually only meant for one specific instance (based on messageId) so that's 1000 instances listening times two (two of those 10 listeners per instance are of the same type) - that's 2k function calls out of which only two will actually process the incoming data because the data was meant for them, the rest will exit early - this approach seems wasteful and at larger scale might slow applications down a quite a bit.

The Solution

We can reduce the amount of calls by directly targeting what handlers to call by constructing a special type runtime:

fig.1:

// current approach
"message.new": [L1, L2, L3... L2000]
// targeted event approach
"message.new-<parentMessageId>": [L1, L2]

In Thread class we know, that for certain handlers we'd only like to run them if the event payload matches parentMessageId (new incoming reply), we don't care about the rest.

So knowing the parentMessageId at the Thread construction time, we can register our handlers with our pre-defined type:

client.on(`message.new-${parentMessageId}`, (event) => {
    // handle event payload
})

At the same time we should register the factory function which instructs the dispatcher how to construct the special type based on the incoming event and store it for the specific type. These factories should be either static methods or simply live outside class scope so their signatures are the same across instances as their outputs should only depend on the input event argument and pre-defined factors (such as event types).

// lives outside class
const messageNewFactory: TargetFactory<DefaultGenerics, 'message.new'> = (event) => {
  return event.parent_id ? `message.new-${event.parent_id}` : null;
};

// register mechanism uses Set under the hood so only one factory-per-signature is accounted for
this.client.registerTargetFactory('message.new', messageNewFactory)

Now when it comes to dispatching the handlers, the dispatcher pulls in registered factories by incoming event type, runs them all and after looks for handlers stored by type (now using custom types) and since only two of such event handlers were registered for that type, it runs only two (refer to fig.1). Total of 3 function calls (including the factory call) instead of 2000.

github-actions[bot] commented 1 month ago

Size Change: +1.21 kB (+0.26%)

Total Size: 459 kB

Filename Size Change
dist/browser.es.js 100 kB +248 B (+0.25%)
dist/browser.full-bundle.min.js 56.8 kB +204 B (+0.36%)
dist/browser.js 101 kB +256 B (+0.25%)
dist/index.es.js 100 kB +247 B (+0.25%)
dist/index.js 101 kB +258 B (+0.26%)

compressed-size-action