Datasource

A datasource abstraction for reading, observing and writing structured application data.

Import
the javascript logo
import { Datasource } from "@schukai/monster/source/data/datasource.mjs";
Source
the git logo
Package
the npm logo
Since
1.22.0

Introduction

Datasource is the base abstraction for application data in Monster. It gives different backends the same contract for options, observation, reading, writing and publishing the current subject.

Core Responsibilities

SurfaceWhat it solves
read()Loads data from the underlying source and updates the current subject.
write()Persists the current subject back to the underlying source.
get() and set()Expose and replace the current real subject in memory.
attachObserver()Lets UI and orchestration code react to subject changes.
setOption() and setOptions()Configure transport, caching or feature flags without changing the class API.

Why The Base Class Matters

The base class keeps local memory stores, REST-backed datasources, storage-backed datasources and realtime transports structurally similar. This lets components and controllers treat them as a shared contract rather than a set of unrelated backends.

First Things To Understand

  • Current subject: get() returns the current in-memory data.
  • Observation: attachObserver() listens to subject changes, not transport-level events.
  • Features: features.skipNoopEvents suppresses redundant notifications when the subject did not actually change.
  • Subclassing: derived datasources implement read() and write() for the actual backend.

Implement A Small In Memory Datasource With Read And Write

import { Datasource } from "@schukai/monster/source/data/datasource.mjs";

class MemoryDatasource extends Datasource {
    async read() {
        this.set({
            title: "Documentation",
            state: "ready",
        });
        return this.get();
    }

    async write() {
        return JSON.parse(JSON.stringify(this.get()));
    }
}

const datasource = new MemoryDatasource();
const output = document.getElementById("datasource-basic-output");

document.getElementById("datasource-basic-read").addEventListener("click", async () => {
    const result = await datasource.read();
    output.textContent = JSON.stringify(result, null, 2);
});

document.getElementById("datasource-basic-write").addEventListener("click", async () => {
    datasource.set({
        title: "Documentation",
        state: "published",
    });
    output.textContent = JSON.stringify(await datasource.write(), null, 2);
});
Open in playground

Attach Observers And React To Subject Changes

import { Datasource } from "@schukai/monster/source/data/datasource.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

class QueueDatasource extends Datasource {
    async read() {
        this.set({
            jobs: [
                { id: "a", state: "queued" },
            ],
        });
        return this.get();
    }

    async write() {
        return this.get();
    }
}

const datasource = new QueueDatasource();
const output = document.getElementById("datasource-observer-output");

const render = () => {
    output.textContent = JSON.stringify(datasource.get(), null, 2);
};

datasource.attachObserver(new Observer(render));
await datasource.read();
render();

document.getElementById("datasource-observer-queue").addEventListener("click", () => {
    datasource.set({
        jobs: [
            ...datasource.get().jobs,
            { id: crypto.randomUUID().slice(0, 6), state: "queued" },
        ],
    });
});

document.getElementById("datasource-observer-done").addEventListener("click", () => {
    datasource.set({
        jobs: datasource.get().jobs.map((entry, index) => ({
            ...entry,
            state: index === 0 ? "done" : entry.state,
        })),
    });
});
Open in playground

Compare SkipNoopEvents With Repeated Writes

import { Datasource } from "@schukai/monster/source/data/datasource.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

class NoopDatasource extends Datasource {
    async read() {
        return this.get();
    }

    async write() {
        return this.get();
    }
}

const output = document.getElementById("datasource-noop-output");

const createDatasource = (skipNoopEvents) => {
    const datasource = new NoopDatasource();
    datasource.setOption("features.skipNoopEvents", skipNoopEvents);
    datasource.set({ state: "idle" });
    return datasource;
};

const defaultDatasource = createDatasource(true);
const forcedDatasource = createDatasource(false);

let defaultCount = 0;
let forcedCount = 0;

defaultDatasource.attachObserver(new Observer(() => {
    defaultCount += 1;
    render();
}));

forcedDatasource.attachObserver(new Observer(() => {
    forcedCount += 1;
    render();
}));

const render = () => {
    output.textContent = JSON.stringify({
        withSkipNoopEvents: defaultCount,
        withoutSkipNoopEvents: forcedCount,
    }, null, 2);
};

document.getElementById("datasource-noop-default").addEventListener("click", () => {
    defaultDatasource.set({ state: "idle" });
    render();
});

document.getElementById("datasource-noop-forced").addEventListener("click", () => {
    forcedDatasource.set({ state: "idle" });
    render();
});

render();
Open in playground

Drive A Small UI List From Datasource Updates

import { Datasource } from "@schukai/monster/source/data/datasource.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

class TaskDatasource extends Datasource {
    async read() {
        this.set({
            tasks: [
                { id: "draft", label: "Draft overview" },
                { id: "sync", label: "Sync generated pages" },
            ],
        });
        return this.get();
    }

    async write() {
        return this.get();
    }
}

const datasource = new TaskDatasource();
const list = document.getElementById("datasource-list-output");

const render = () => {
    list.innerHTML = "";
    for (const task of datasource.get().tasks ?? []) {
        const item = document.createElement("li");
        item.textContent = task.label;
        list.appendChild(item);
    }
};

datasource.attachObserver(new Observer(render));
await datasource.read();

document.getElementById("datasource-list-add").addEventListener("click", () => {
    datasource.set({
        tasks: [
            ...(datasource.get().tasks ?? []),
            { id: crypto.randomUUID().slice(0, 6), label: "Publish release" },
        ],
    });
});

document.getElementById("datasource-list-remove").addEventListener("click", () => {
    datasource.set({
        tasks: (datasource.get().tasks ?? []).slice(1),
    });
});
Open in playground

Use Datasource Options To Control Async Reads

import { Datasource } from "@schukai/monster/source/data/datasource.mjs";

class AsyncDatasource extends Datasource {
    async read() {
        const latency = this.getOption("transport.latency", 300);
        const label = this.getOption("transport.label", "default");

        await new Promise((resolve) => {
            setTimeout(resolve, latency);
        });

        this.set({
            label,
            latency,
            loadedAt: new Date().toISOString(),
        });

        return this.get();
    }

    async write() {
        return this.get();
    }
}

const datasource = new AsyncDatasource();
const output = document.getElementById("datasource-async-output");

const runRead = async (latency, label) => {
    datasource.setOptions({
        transport: {
            latency,
            label,
        },
    });

    output.textContent = "loading...";
    output.textContent = JSON.stringify(await datasource.read(), null, 2);
};

document.getElementById("datasource-async-fast").addEventListener("click", () => runRead(120, "fast"));
document.getElementById("datasource-async-slow").addEventListener("click", () => runRead(700, "slow"));
Open in playground

Exported

Datasource

Derived from

Base

Options

The Options listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits Options from these parent classes. If you cannot find a specific Options in this list, we recommend consulting the documentation of the Base.

Option
Type
Default
Description
-/-

Properties

The Properties listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits Properties from these parent classes. If you cannot find a specific Properties in this list, we recommend consulting the documentation of the Base.

Methods

The methods listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits methods from these parent classes. If you cannot find a specific method in this list, we recommend consulting the documentation of the Base.

Constructor

constructor()
creates a new datasource

Structural methods

get()
Returns
  • {Object|Array}
Returns real object
getOption(path,defaultValue)
Parameters
  • path {string}: path
  • defaultValue {*}: defaultValue
Returns
  • {*}
nested options can be specified by path a.b.c
set(data)
Parameters
  • data {object|array}: data
Returns
  • {Datasource}
setOption(path,value)
Parameters
  • path {string}: path
  • value {*}: value
Returns
  • {Datasource}
Set option
setOptions(options)
Parameters
  • options {string|object}: options
Returns
  • {Datasource}
Throws
  • {Error} the options does not contain a valid json definition

Static methods

[instanceSymbol]()2.1.0
Returns
  • {symbol}
This method is called by the instanceof operator.

Other methods

attachObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {Datasource}
attach a new observer
containsObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {boolean}
detachObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {Datasource}
detach a observer
read()
Returns
  • {Promise}
Throws
  • {Error} this method must be implemented by derived classes.
write()
Returns
  • {Promise}
Throws
  • {Error} this method must be implemented by derived classes.

Events

This component does not fire any public events. It may fire events that are inherited from its parent classes.

The current width of the area is too small to display the content correctly.