Type quickly, then stop
Every keystroke is written to query at once - the count on the left climbs with each one. What rateLimit holds
back is the announcement, so anything depending on query (here the "heard" readout; on a real page, a search request)
runs once, when the typing stops. Switch to "at a fixed rate" and a continuous stream announces once per half-second
instead.
- Keystrokes written
- 0
- Announcements
- 0
- Heard
- -
/**
* `rateLimit` - hold the notification, never the write.
*
* Every keystroke is written to `query` at once. What the extender holds back
* is the announcement, so the "heard" readout (and anything else that depends
* on `query` - a search request, say) runs once, when the typing stops.
*/
function rateLimited() {
const method = observable('notifyWhenChangesStop');
const query = observable('').extend({rateLimit: {timeout: 500, method: method.peek()}});
const keystrokes = observable(0);
const announcements = observable(0);
const heard = observable('');
// A subscriber hears each announcement - after the limiter lets it through.
query.subscribe((value) => {
announcements.value = announcements.peek() + 1;
heard.value = value;
});
// Extending again reconfigures the one limiter rather than stacking another.
method.subscribe((m) => query.extend({rateLimit: {timeout: 500, method: m}}));
return {
query, method, keystrokes, announcements, heard,
typed() { keystrokes.value = keystrokes.peek() + 1; },
reset() {
keystrokes.value = 0;
announcements.value = 0;
heard.value = '';
query.value = '';
}
};
}The built-in extenders
| Extender | Value | Effect |
|---|---|---|
rateLimit |
ms, or {timeout, method} |
hold notifications; method is notifyWhenChangesStop (default) or notifyAtFixedRate |
throttle |
ms | Knockout's older name, given rateLimit's behaviour |
notify |
'always' |
announce every write, even one the change gate would swallow |
Knockout's original throttle delayed the write itself, so reading a throttled observable could return a value that
was already out of date. That is not repeated here: whatever is extended onto an observable, reading it always returns
the last value written. Extending again reconfigures the one limiter rather than nesting a second, and
.extend({rateLimit: 0}) switches it off.
Your own
registerExtender(name, fn) hands your extender exactly two powers - replace the change gate, or wrap the announcement
- and no way to touch the stored value, which keeps that guarantee true of every extender, yours included.
import {registerExtender, observable} from 'domma-reactive';
registerExtender('trace', (control, label) => {
control.intercept((next) => (value) => {
console.log(label, value);
next(value);
});
});
const count = observable(0).extend({trace: 'count'});