Extenders

Hold the notification. Never hold the write.

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
-
apps/features.js - a rate-limited search boxview whole file
/**
 * `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'});

Next: safety All features