@graphql-workbench/execute-query
v0.2.0
Published
A web component for GraphQL variables and consumer-owned query execution
Downloads
34
Readme
Execute Query
<gwb-execute-query> is a Lit web component with a variables pane above a response pane.
The consumer owns the query editor and handles execution through a DOM event.
The component accepts response chunks through an observer-compatible sink or through an
AsyncIterable/ReadableStream property. It can display decoded chunks in arrival order
or assemble direct-path incremental GraphQL chunks into one response. Both views record
arrival timing and bound retained raw chunk history. Variables and responses use Prism
Code Editor's JSON grammar for syntax highlighting and line numbers. The variables
editor also suggests names declared in the consumer's current query.
Install
npm install @graphql-workbench/execute-query @preact/signals-core@preact/signals-core is a peer dependency. Lit and Prism Code Editor are installed as
normal runtime dependencies.
Development
Use Node 22.13+ (or Node 24).
npm ci
npm run dev --workspace=@graphql-workbench/execute-queryRun these commands from the monorepo root.
Open the URL printed by Vite to try the consumer demo. Its query editor and simulated Observable live outside the component. The event's request-bound sink receives four out-of-order direct-path chunks; Cancel request demonstrates consumer unsubscription.
npx playwright install chromium firefox webkit
npm run check --workspace=@graphql-workbench/execute-querycheck runs lint, formatting, type checking, unit/component tests, three library builds,
declaration and packed-consumer checks, bundle budgets, and browser tests. Use
npm run check:node for the non-browser gate or npm run test:e2e for browser coverage.
CI configuration and manual release checks are still to be added.
Module usage
The package entry registers the element and exports ExecuteQuery and its public types.
The snippets below describe local/built package consumption; no package has been published.
<gwb-execute-query id="panel"></gwb-execute-query>import '@graphql-workbench/execute-query';
const panel = document.querySelector('gwb-execute-query')!;
panel.variablesText = '{ "id": "42" }';
panel.query = queryEditor.getValue();
panel.maxChunks = 500;
panel.responseMode = 'merged';
queryEditor.onDidChangeModelContent(() => {
panel.query = queryEditor.getValue();
});
panel.addEventListener('gwb-execute-query', ({ detail }) => {
const { variables, signal, sink } = detail;
const subscription = graphqlClient
.execute({
query: queryEditor.getValue(),
variables,
})
.subscribe(sink);
const unsubscribe = () => subscription.unsubscribe();
if (signal.aborted) unsubscribe();
else signal.addEventListener('abort', unsubscribe, { once: true });
});sink follows the Observable next/error/complete shape. Each method returns
true when the current request accepted the notification and false after it became
stale or terminal. The sink is request-bound, so consumers do not carry a request ID
through every callback.
For browser or generator streams, assign the correlated response property:
panel.addEventListener('gwb-execute-query', async ({ detail }) => {
try {
const stream = await runQueryAsStream({
query: queryEditor.getValue(),
variables: detail.variables,
signal: detail.signal,
});
panel.response = { requestId: detail.requestId, stream };
} catch (error) {
panel.response = {
requestId: detail.requestId,
error: { message: error instanceof Error ? error.message : String(error) },
};
}
});Signal store
createExecuteQueryStore() exposes the execution state machine without requiring a
component. Consumer-controlled inputs are writable Preact signals; lifecycle and output
state are readonly signals. Actions preserve validation, request correlation,
cancellation, source replacement, response retention, merging, and disposal invariants.
Use @graphql-workbench/execute-query/store when only the DOM-free state machine is needed in Node or a
coordinator; the root entry additionally registers web components.
import { createExecuteQueryStore, type ExecuteQuery } from '@graphql-workbench/execute-query';
const store = createExecuteQueryStore();
store.query.value = queryEditor.getValue();
store.variablesText.value = '{ "id": "42" }';
store.responseMode.value = 'merged';
store.events.subscribe('execute-requested', ({ query, variables, signal, sink }) => {
const subscription = graphqlClient.execute({ query, variables }).subscribe(sink);
const unsubscribe = () => subscription.unsubscribe();
if (signal.aborted) unsubscribe();
else signal.addEventListener('abort', unsubscribe, { once: true });
});
store.execute();The store's execute-requested event includes immutable query and variables snapshots.
variables-change is emitted only by editVariables(); assigning
store.variablesText.value directly is programmatic and does not echo. An explicit
cancel() emits execution-cancelled, aborts the request signal, and rejects stale
delivery. Use acceptResponse() for a correlated AsyncIterable, ReadableStream, or
pre-stream error. Without a subscriber or response, the store remains in waiting and
does not invent a timeout. If an execute-requested listener throws, execute()
rethrows that error, ends the request in error, aborts its signal, and permits another
execution. Call dispose() when the owner is finished.
Assign the store to one or more components to share the same execution state:
const first = document.querySelector<ExecuteQuery>('#first')!;
const second = document.querySelector<ExecuteQuery>('#second')!;
first.store = store;
second.store = store;An injected store is authoritative and remains owned by the consumer: disconnecting or
rebinding a view does not cancel or dispose it. With no injected store, <gwb-execute-query>
creates its own store and retains the standalone DOM-event contract. A component-originated
execution publishes execute-requested first and then emits gwb-execute-query once from the
originating component. Connect a transport to either the typed store event or the
compatibility DOM event, not both, to avoid executing the request twice. Direct store
actions update bound views without synthesizing DOM events.
Use store.snapshot() for persistence and store.restore(value) to restore durable
execution preferences. Snapshots are versioned, detached, deeply frozen, and
JSON-serializable. They retain query and variables editor text—including invalid
variables text—plus response mode, theme, disabled preference, and chunk limit. Request
IDs, sinks, streams, output, errors, timing, and active status are excluded. Restoring
an active store cancels its source and aborts its signal, then resets execution to
idle without emitting a cancellation event. Use
parseExecuteQueryStoreSnapshot(value) to prevalidate a slice before mutating it.
Direct pane composition
Consumers that own the execution state machine can instantiate the variables and response panes separately. Importing the package registers both custom elements and exports their classes, parsing helpers, and display model types.
The panes can share the narrow slices of an ExecuteQueryStore directly:
variables.store = store;
response.store = store;In this mode, the variables pane applies user edits and starts execution through the
store, while the response pane renders its authoritative lifecycle and response state.
The root component keeps its internal panes controlled rather than injecting the store
into them, preventing duplicate execution.
In a composed workbench, @graphql-workbench/graphql-workbench-store owns this execution slice and the
schema slice. Pass workbench.execution to the root component or directly to both
panes, but do not bind the same panes independently inside <gwb-execute-query>. Keep the
query editor and transport consumer-owned. A schema selection does not modify query
text. Dispose the workbench only after all borrowing views are finished.
<gwb-variables-pane id="variables"></gwb-variables-pane>
<gwb-response-pane id="response"></gwb-response-pane>import {
extractVariableNames,
parseVariables,
type ResponseChunk,
type ResponsePane,
type VariablesPane,
} from '@graphql-workbench/execute-query';
const variables = document.querySelector<VariablesPane>('#variables')!;
const response = document.querySelector<ResponsePane>('#response')!;
variables.variableNames = extractVariableNames('query Viewer($id: ID!) { viewer(id: $id) { id } }');
variables.addEventListener('gwb-variables-input', ({ detail }) => {
variables.value = detail.value;
variables.error = '';
});
variables.addEventListener('gwb-execute', () => {
const value = parseVariables(variables.value);
response.status = 'waiting';
// Execute with value, then assign immutable ResponseChunk records and terminal state.
});VariablesPane exposes store, value, variableNames, error, disabled, and
focusEditor(). It emits bubbling, composed gwb-variables-input and gwb-execute
events. ResponsePane renders consumer-owned status, mode, chunks, truncation,
error, timing, and merged-response properties, or accepts a store that is authoritative
over those presentation properties. Without a pane store, direct consumers own request
IDs, cancellation, timing, response serialization, and immutable state updates; use the
root <gwb-execute-query> element when those behaviors should be supplied by the package.
Variables must be a JSON object; blank input means {}. Invalid input focuses the editor
and shows an inline error without emitting an execution event. Each execution supplies
a new request ID and a deeply frozen variables snapshot. User edits emit
gwb-variables-change with { value }; programmatic assignments do not emit that event.
Both public events bubble and cross shadow boundaries.
Set query whenever the consumer-owned query editor changes. The component scans its
operation variable declarations and suggests them for top-level variables object keys.
Typing filters the list; Ctrl+Space opens it explicitly, arrow keys move through it,
Enter or Tab accepts a name, and Escape closes it. For documents containing multiple
operations, suggestions include the unique declarations from all operations. The query
is metadata for suggestions only and is not added to gwb-execute-query event details.
The button disables while waiting or streaming, or when panel.disabled is true.
Variables remain editable for the next request. Setting disabled does not abort an
active request. panel.cancel() aborts the event signal and cancels an accepted stream.
Observable consumers connect that signal to unsubscribe() as shown above. Removing
a standalone component cancels its private active request; removing a view backed by an
injected store leaves the shared request running.
Cmd/Ctrl+Enter in the variables editor follows the same execution rules as a click.
The component does not execute requests, retry, or set a timeout on the consumer's behalf.
Public API
| Member | Type/default |
| ----------------- | ---------------------------------------------------------------------------- |
| variablesText | Property only; string, default '{}' |
| query | Property only; string, default ''; supplies variable-name suggestions |
| response | Property only; correlated ResponseInput, default null |
| responseMode | Reflected response-mode; 'chunks' \| 'merged', default 'chunks' |
| theme | Reflected attribute; 'auto' \| 'light' \| 'dark', default 'auto' |
| disabled | Boolean property/attribute; default false |
| maxChunks | Number property/max-chunks attribute; default 500, minimum effective 1 |
| status | Readonly idle, waiting, streaming, complete, error, or cancelled |
| executionTimeMs | Readonly total duration in milliseconds; null until a run ends |
| store | Property only; optional consumer-owned ExecuteQueryStore, default null |
| cancel() | Cancels a waiting/streaming execution and preserves chunks already received |
Public types also include ResponseSink, ResponseInput, ResponseStream,
ResponseMode, ResponseChunk, ExecuteQueryStore, VariablesPaneStore,
ResponsePaneStore, ExecuteQueryStoreEventMap,
ExecuteRequestedDetail, ExecutionCancelledDetail, VariablesPaneInputDetail, and
the pane input types.
document.createElement('gwb-execute-query') infers ExecuteQuery, and event details are
typed through HTMLElementEventMap.
Each accepted chunk is serialized immediately as a display snapshot. GraphQL payloads
containing both data and errors remain ordinary chunks. A source failure preserves
earlier chunks and displays an error. Stream exhaustion completes the response, even
with zero chunks; fields such as hasNext do not control consumption. Assigning a new
source for the active request replaces the previous source and clears its output. Stale
request IDs, old sinks, and delayed values from replaced sources are ignored.
The component retains the newest maxChunks blocks and displays how many earlier
chunks were omitted while keeping their original sequence numbers. Numbering starts at
1 for each execution and restarts when its response source is replaced. Invalid, non-finite,
or missing limits use 500; finite values are floored and clamped to at least 1.
Changing the limit trims displayed output immediately, but cannot restore chunks that
were already omitted. Same-turn emissions are batched into one reactive update.
Each chunk shows its UTC arrival time and milliseconds elapsed since execution began.
The component starts its monotonic timer immediately before dispatching gwb-execute-query
and stops it on completion, failure, explicit cancellation, or disconnection. Stream
replacement keeps the original execution start. executionTimeMs exposes the rounded
total duration after a terminal state and resets to null when the next valid execution
begins. Wall-clock changes can affect the displayed UTC timestamp but cannot make elapsed
or total durations negative.
Inputs must contain decoded, JSON-serializable values. Response inputs use unknown so
typed GraphQL result interfaces can be passed directly to Observable and stream APIs;
values are serialized when received. Consumers remain responsible for HTTP, SSE, or
multipart framing.
Merged response mode
Set responseMode = 'merged' or response-mode="merged" before execution to assemble
one response. The value is captured when execution starts, so changing it during an
active request affects the next request. The default chunks mode retains the original
one-block-per-chunk behavior.
Merged mode supports decoded direct-path chunks of this form:
sink.next({ data: { post: { title: 'Post', comments: [] } } }); // primary: no path
sink.next({ data: { id: 'c2' }, path: ['post', 'comments', 1] });
sink.next({ data: { id: 'c1' }, path: ['post', 'comments', 0] });
sink.next({
data: null,
path: ['post', 'comments', 2],
errors: [{ message: 'Comment failed', path: ['post', 'comments', 2] }],
});
sink.complete();Paths are relative to the primary chunk's data. String segments select object fields;
non-negative integers select array positions. Object payloads merge recursively over
existing object data, while arrays, scalars, and null replace the selected value.
Patch errors are appended to the assembled top-level errors array.
The merged response gutter marks when each field or array item arrived. GraphQL errors
with a valid path receive an error marker on that field; errors without a path are
marked on the top-level errors entry. Focus or hover a marker for its chunk number,
arrival time, elapsed time, or error message. The markers remain available when
maxChunks trims raw chunk records.
Chunks may arrive before the primary chunk or before a required parent or earlier array
item. The component buffers them and retries after each arrival. It never publishes a
sparse array: index 2 remains buffered until indices 0 and 1 are present. An explicit
data: null, including one paired with GraphQL errors, resolves its position. Completion
without a primary chunk or with unresolved predecessors ends in error and preserves
the assembled partial response.
This mode supports the direct data + path shape above. It does not decode transports
or implement the ID-based pending/incremental/completed defer/stream envelope;
adapt that protocol to direct-path chunks in consumer code.
Themes and layout
gwb-execute-query {
--gwb-eq-font-size: 14px;
--gwb-eq-variables-height: 160px;
--gwb-eq-action-bg: #0058d0;
}theme="auto" follows the system. Set light or dark explicitly to override it.
--gwb-eq-height defaults to auto; an explicit height bounds the flex layout.
--gwb-eq-variables-height defaults to 144px, with an 80px minimum and vertical resizing.
Use enough component height to accommodate the variables pane and response header.
The response pane uses the remaining height when bounded. Typography defaults to 13px
and a JetBrains Mono/system monospace stack; the component downloads no fonts.
Color tokens: --gwb-eq-bg, --gwb-eq-header-bg, --gwb-eq-text-primary,
--gwb-eq-text-secondary, --gwb-eq-border, --gwb-eq-action-bg, --gwb-eq-action-text,
--gwb-eq-focus-ring, --gwb-eq-error-text.
Public parts: variables-pane, variables-editor, execute-button, response-pane,
response-content, status, error, truncation, duration, chunk-timing,
merged-response, merge-pending, code-view, annotation-marker, and
annotation-tooltip.
Standalone browser bundle
<script src="dist/execute-query.umd.js"></script>
<gwb-execute-query></gwb-execute-query>UMD bundles Lit, Preact Signals Core, the shared event channel, and Prism Code Editor. It
exposes the component as ExecuteQuery.ExecuteQuery and the store factory as
ExecuteQuery.createExecuteQueryStore. ES and CommonJS externalize Lit, Signals Core,
and @graphql-workbench/graphql-store-core while bundling the JSON response renderer. CommonJS is intended
for browser-capable, ESM-aware bundlers; the root entry registers web components and is
not a DOM-free server entry. npm run build also emits TypeScript declarations and
source maps.
See AGENTS.md for project guidance and DESIGN.md for the full streaming contract and remaining design choices.
