Reading time: 6 minutes and 45 seconds.

Working with mod readers

This document gives an introduction to the mod reader.

The Spotfire Mod API exposes a set of Readables, such as the DataView and properties. These can be consumed by a Reader in order to read their values and subscribe to changes.

mod.createReader(mod.property("myProperty"), mod.windowSize());

Introduction

The aim of a reader is to simplify the synchronization of reading related values. An example is a visualization that depends on the window’s size and the dataview. When one of the two changes, the visualization needs to be redrawn. The reader fetches new values and keeps unchanged values in memory.

The API exposes readables for the Data, Properties, Document Properties, Pages, Window size, Axes, Tables and Columns.

Methods

A reader has three methods; subscribe, once and hasExpired.

subscribe(callback)

Subscribe will be the most common way to work with readables. It sets up a subscription to new changes of the readables provided to the reader. When one or more readables gets updated on the server, the reader fetch all new values and invoke the provided callback with updated values.

The reader will only allow one subscribe callback to be active at a time. This means that the callback will finish executing before a new callback is scheduled. The following example shows how the callback will not be invoked again until its returning promise has settled. Even if the dataView is changed by marking or filtering. After the callback has finished executing, the callback is invoked again if there are any changed values. The hasExpired method can be used to check whether any readables have been invalidated while the callback has been executing.

const reader = mod.createReader(mod.visualization.data());

reader.subscribe(async function render(dataView) {
    log(
        "Subscribe callback invoked, ",
        (await dataView.allRows()).length,
        " rows. Start 3 seconds sleep."
    );

    await sleep(3000);

    log(
        "Render callback finished.",
        "Has the reader while the callback was executed: ",
        await reader.hasExpired()
    );
});

Demonstration of subscribe callback

once(callback)

When a developer wants to control the pace of updates, the once method can be used to wait for changes. Once will invoke its provided callback a single time, when one or more of the provided readables have changed.

let loopsRunning = 0;
let count = 0;
const reader = mod.createReader(mod.visualization.data());

reader.once(async function render(dataView) {
    loopsRunning++;
    let id = ++count;

    await dataView.allRows();
    log(
        "Render callback invoked with id: " + id,
        ". Currently " + loopsRunning + " loops active.",
        "schedule once() again and start 3 seconds sleep."
    );

    reader.once(render);

    await sleep(3000);

    log("Render callback finished with id:" + id);

    loopsRunning--;
});

Demonstration of once callback

hasExpired()

The reader keeps track of whether its readables are up to date or not. The hasExpired method can be invoked if you want to check whether all fetched values are up to date. This can be useful if your rendering loop takes a long time to complete and you want to quickly respond to changes.

The following example shows how a reader becomes expired after a mod property has been modified.

const reader = mod.createReader(mod.property("myProperty"));

reader.once(async function render(prop) {
    log("Render callback invoked ", prop.value());

    mod.transaction(
        () => {
            prop.set("next value" + Math.random());
        },
        async () => {
            log(
                "Transaction complete. Reader has expired: ",
                await reader.hasExpired()
            );
        }
    );

    log("End of callback. Reader has expired: ", await reader.hasExpired());
});

In this example the slow render loop is synthesized through a set of sleeps. Each time the mod is clicked, a mod property is updated and trigger a render loop. Since subscribe will only invoke the callback after it has finished, the hasExpired method can be used to trigger an early return of the render callback.

const reader = mod.createReader(mod.property("myProperty"));

document.body.onclick = () => {
    log("Set new value for myProperty ");
    mod.property("myProperty").set("next value" + Math.random());
};

reader.subscribe(async function slowRender(prop) {
    log("Slow render started ", prop.value());
    await sleep(3000);

    if (await reader.hasExpired()) {
        log("Reader has expired, return early to enter new loop");
        return;
    }

    await sleep(3000);

    log("End of slow rendering");
});

function sleep(ms) {
    return new Promise((res) => setTimeout(res, ms));
}

function log(...messages) {
    let div = document.createElement("div");
    div.textContent = messages.join(" ");
    document.body.appendChild(div);
    div.scrollIntoView();
}

Guidance on how to use readers

  • If two or more readables depend on each other, they should be part of the same reader. Separating readers from a performance concern is often a pre-optimization. The reader is keeping unchanged values in memory and only changed values are fetched. If there is a performance concern it can be better to check which value has changed through reference equality with the previous value.

  • If you need an initial setup, this can be done in a once callback, followed by setting up a subscribe callback.

  • Use hasExpired() when your render loop takes a long time, in order to respond quicker to changes.

Separating initial setup from updates

If a mod needs some initial setup before starting to listen for changes, reader.once can be used with a setup callback.

A reader will only trigger a new callback if any of its Readables has changed since it was last part of a reader callback. That means we can separate the setup from upcoming updates without triggering unnecessary callbacks.

In the following example, there are three Readables. They are first read together in an initial once callback as a setup for the mod. They are afterwards separated into two readers, one for the property and one for the rendering loop. Neither of the subscription callbacks will be invoked immediately. Instead they will wait until one of the readables change.

const setupReader = mod.createReader(
    mod.visualization.data(),
    mod.windowSize(),
    mod.property("myProperty")
);

setupReader.once((dv, size, prop) => {
    log("All values are read", prop.value());

    const propertyReader = mod.createReader(mod.property("myProperty"));
    propertyReader.subscribe((myProperty) => {
        log("Property has changed to", myProperty.value());
    });

    const renderReader = mod.createReader(
        mod.visualization.data(),
        mod.windowSize()
    );
    renderReader.subscribe(async function render(dataView, windowSize) {
        log(
            "Render",
            (await dataView.allRows()).length,
            windowSize.height
        );
    });

    setInterval(() => prop.set("New value" + Math.random()), 2000);
});

A readable can only be part of one reader at a time. subscribe and once will not invoke the callback until the readable changes

Knowing what changed

Sometimes you want to know what changed inside of a subscribe callback, often in order to improve the performance of the mod.

  • Reference equality
  • Unchanged values are reused.

Note. It might be an unnecessary sub optimization to check for changes.

let reader = readerWithChangeChecker(
    mod.createReader(
        mod.visualization.data(),
        mod.windowSize(),
        mod.property("myProperty")
    )
);

reader.subscribe(async (dataview, size, prop) => {
    console.log("Dataview", reader.hasValueChanged(dataview));
    console.log("size", reader.hasValueChanged(size));
    console.log("property", reader.hasValueChanged(prop));
    await dataview.allRows();
    setTimeout(() => prop.set("New value" + Math.random()), 2000);
});

/**
 * Wrap a reader with an additional method called `hasChanged`.
 * It allows you to check whether a value is new or unchanged since the last time the subscribe loop was called.
 * @function
 * @template A
 * @param {A} reader
 * @returns {A & {hasValueChanged(value: any):boolean}}
 */
function readerWithChangeChecker(reader) {
    let previousValues = [];
    let currentValues = [];
    function compareWithPreviousValues(cb) {
        return function compareWithPreviousValues(...values) {
            previousValues = currentValues;
            currentValues = values;
            return cb(...values);
        };
    }

    return {
        ...reader,
        subscribe(cb) {
            // @ts-ignore
            reader.subscribe(compareWithPreviousValues(cb));
        },
        hasValueChanged(value) {
            return previousValues.indexOf(value) == -1;
        }
    };
}

Readables

Every set of data that can be consumed through the Mod API is encapsulated in a Readable. A Readable can be seen as a Promise combined with an identifier. In contrast to Promises, which are eager, Readables are lazy. It is lazy because it does not immediately start fetching its promised value. It only fetches the value it encapsulates if it is awaited or the then method is invoked.

// This does not fetch any value from the server
const myPropertyReadable = mod.property("myProperty");
console.log(myPropertyReadable);

// This fetches the latest value from the server.
const myProperty = await mod.property("myProperty");
console.log(myProperty);

ReadableProxy

If a value has synchronous methods, e.g a mod property, a ReadableProxy will be returned from the API instead of a Readable. The ReadableProxy is a Readable augmented with all synchronous methods of the encapsulated value, without the need to await the encapuslated value. This means a property can be set, without knowing its value.

In the following example the mod property myProperty is modified without awaiting its encapsulated value.

mod.property("myProperty").set("new value");
  • ReadableProxy augment a Readable/Promise with additional methods.
  • Methods are accessible without awaiting the Readable.
  • Value can be awaited. Will always fetch the latest up to date value.
  • Modifications should be done on user interaction, not directly in a reader callback.

Full examples

All examples can bee downloaded here. They can be tested with one of the existing examples, such as the js-dev-starter.

Last modified June 5, 2024