The schema module and QgisSdk
client.invoke<T>(...) is a transport, not a type guarantee: T is asserted by
hand at the call site. For operations, generate the plugin’s schema module
from its own declarations and build a QgisSdk on it
(decision-44):
$ qgis-sdk bridge generate --plugin my_plugin.plugin:appwrote web/src/bridge.schema.tsWhat does the module export?
Section titled “What does the module export?”The module is data, not code. It exports the declared value types, OPERATIONS
and OperationId, one Operations interface (request and result per action),
one Streams interface (request and event union per stream), and as its
default export the parsed plugin description typed by both:
import { type BridgeSchema, defineBridgeSchema } from "@archont561/qgis-sdk";
export interface Operations { readonly "sites.summary": { readonly request: SitesSummaryRequest; readonly result: SitesSummaryResult }; // ...}export interface Streams { readonly "sites.watch": { readonly request: SitesWatchRequest; readonly event: SitesWatchEvent };}const schema: BridgeSchema<Operations, Streams> = defineBridgeSchema<Operations, Streams>({ /* description */ });export default schema;How does a page build a QgisSdk?
Section titled “How does a page build a QgisSdk?”defineBridgeSchema is the module’s only import. It parses and freezes the
description exactly as parsePluginDescription does and pins the two type
parameters; there is no generated validator, because the SDK validates at run
time from the schemas the description already carries. A page then needs
three statements:
import { QgisSdk } from "@archont561/qgis-sdk";import schema from "./bridge.schema";
const qgis = QgisSdk.fromBridgeSchema(schema);const summary = await qgis.sites.summary({ layer_id: "sites" });How does it connect, and what does it check?
Section titled “How does it connect, and what does it check?”Transport
Section titled “Transport”Without options fromBridgeSchema connects to the QWebChannel object named
"bridge", which is what the Python runtime registers on the view. Outside a
QGIS web view it throws QgisSdkWebChannelError (code: "no_web_channel")
synchronously, so the page can say why instead of waiting on a channel that
never comes. Pass { transport } to use another wire (the test harness below),
{ client } to share a BridgeClient the page already owns (then close()
leaves it alone), { channelObject } for another registered name, { timeoutMs } for the per-call timeout of an owned client.
Compile time
Section titled “Compile time”Every declared operation is a method nested under the segments of its id:
qgis.open_fixture() for a plain id, qgis.sites.summary(...) for
sites.summary, qgis.tools.measure.area(...) for a deeper one - the same
shape as qgis.host.layers.list(...). An operation the plugin did not declare
is not a member, and a misspelled or mistyped argument is a type error -
including an unknown property inside a nested object. An action whose request
declares no fields takes no argument. Two rules follow from nesting, checked by
bridge generate and again by fromBridgeSchema (QgisSdkDeclarationError):
an id may not be a prefix of another (sites cannot be a method and hold
sites.summary), and an id’s first segment may not be a core member (client,
host, schema, ready, close).
Runtime
Section titled “Runtime”The method validates what it sends (strict: unknown arguments are refused
before the transport is touched) and what comes back (lenient: a field a newer
host added is tolerated, a missing or mistyped one is refused). Both refusals
are QgisSdkContractError (code: "contract_validation_failed") naming the
operation, the direction (request, result or event) and the issues, each
with a path. Primitives, list/tuple, unions, Literal and nested
dataclasses are checked; a reply the schema cannot describe never reaches the
page.
Streams are operations too (decision-28)
Section titled “Streams are operations too (decision-28)”A method the plugin marked with @stream(id="sites.watch", events={"data": SiteChange, "progress": ScanProgress}) becomes qgis.sites.watch(request, { onEvent, onTerminal }, options), returning the same StreamHandle as
client.subscribe. onEvent narrows on event.kind; a frame whose kind the
plugin did not declare, or whose payload does not match, ends the subscription
with an error terminal instead of reaching the handler. The frame envelope -
seq, gap, close() - stays in this package’s kernel.
What else is on the instance?
Section titled “What else is on the instance?”qgis.host
Section titled “qgis.host”qgis.host is the typed host surface of Host namespaces on
the same client - layers, project, settings, notifications, styles,
files and the network.fetch stream - plus granted, the host operation ids
the handshake advertised for this view. A call outside the grants is refused by
the host with its own failure code; the SDK does not pre-empt it.
qgis.ready
Section titled “qgis.ready”qgis.ready resolves once the handshake answered; it rejects with the host’s
QgisSdkBridgeRemoteError when the host refused the handshake and with
QgisSdkBridgeProtocolError when the reply is not a handshake; qgis.client
is the BridgeClient underneath for what no method covers; qgis.schema is
the module the instance was built from. A plugin may not declare an operation
named like one of these members (QgisSdkDeclarationError).
Owned by CI
Section titled “Owned by CI”The file is generated source, never edited by hand: the plugin’s CI runs
qgis-sdk bridge generate --check and fails when the checked-in copy differs.
CONTRACT_FINGERPRINT identifies the wire contract the file was built from.
const handle = await qgis.sites.watch( { layer_id: "sites" }, { onEvent: (event) => { if (event.kind === "data") render(event.payload.change); }, onTerminal: (terminal) => console.info("stream ended:", terminal.kind), },);The 0.3.0 generated client
Section titled “The 0.3.0 generated client”qgis-sdk bridge generate --out web/src/bridge.ts still renders what 0.3.0
rendered - the import-free module with createOperations(client),
PluginOperations and one validator per type - and prints a note that the
schema module is the current form. It stays for one minor. A page on the
legacy client keeps working unchanged.