Skip to content

Stores: the framework-free core

A page’s state is in four stores exported from the root, each a Store<T>: getSnapshot() returns a stable, frozen snapshot and subscribe(listener) calls the listener once synchronously and on every change, returning an unsubscribe. Both are bound methods, so they can be handed to useSyncExternalStore, a Svelte $store or a plain addEventListener-style page unchanged. The stores import no framework and no browser global; they are what the three adapters and the scaffold’s vanilla page are built on.

import { QgisSdk, formStore, layersQuery } from "@archont561/qgis-sdk";
import schema from "./bridge.schema";
const qgis = QgisSdk.fromBridgeSchema(schema);
// A form over the declared `buffer` action: fields from the request schema.
const buffer = formStore(qgis, "buffer", { initial: { distance_metres: 500 } });
for (const field of buffer.fields) {
// field.name, field.label ("Distance metres"), field.kind ("metres"),
// field.required, field.choices, field.details ({ minimum, maximum, ... })
}
buffer.set("distance_metres", "750"); // strings are coerced per kind
buffer.subscribe((state) => {
// state.values, state.issues, state.errors (first code per field),
// state.valid, state.dirty, state.submitted, state.submission
});
await buffer.submit(); // validates first; an invalid form never calls the host
// A layer picker over the curated host surface, grant first.
const layers = layersQuery(qgis);
layers.subscribe((state) => {
// idle | loading | ok (value: VectorLayerInfo[]) | error | refused
});
void layers.refresh();

Choose a store for an operation, stream, form or host query.

One declared operation as state: idle | pending | ok (value) | error (error). run(input) resolves with the value or undefined (failed, aborted or superseded; the state carries the error, so the promise never rejects into an event handler); only the latest run can change the state. abort(), reset().

One subscription that follows the content of its input (setInput(value), keyed on stableKey; null closes it): idle | opening | open | ended | failed. setHandlers swaps the callbacks without reopening; close() sends one unsubscribe even before the host has acknowledged the open.

FormStore / formStore(qgis, actionId, { initial })

Section titled “FormStore / formStore(qgis, actionId, { initial })”

The typed form for one declared action, built from its request schema with schemaFields(schema): a FormField per property with the parameter kind the Python side marked ("x-qgis-sdk", doc-12), a labelFor label, required, nullable, choices and details. set(name, value) coerces strings per kind (numbers, booleans, integers) and validates with validateAgainstSchema; setValues, reset(), submit() (validates, marks submitted, then runs the operation), abort(), dispose(), field(name). The declared dialect has no defaults, so starting values come from initial.

HostQueryStore(qgis, hostOperationId, loader)

Section titled “HostQueryStore(qgis, hostOperationId, loader)”

Reads the curated host surface after the handshake. When qgis.host.granted lacks the operation the query needs it settles on refused (with the operation id and a message) without a call, so a page can say “add layers.read to the page’s grants” instead of surfacing a not_authorized fault. layersQuery(qgis, { pageSize? }) follows layers.list to the last page; layerFieldsQuery(qgis, layerId) reads layers.get for a field input whose of names a layer input. Both need the layers.read grant, which the scaffold’s [tool.qgis-sdk.web] grants now lists.

A QgisSdkStoreError ("store_error") is raised for misuse: a form over an action the schema does not declare, or a field the form does not have. The behaviour suite in tests/store-behaviour.ts runs the same ten expectations against the core and against each adapter’s driver, so an adapter cannot drift from the stores.