Skip to content

Commit 729ec24

Browse files
committed
fix: background query overwrote yasr instead of extending it
1 parent e14760d commit 729ec24

8 files changed

Lines changed: 289 additions & 20 deletions

File tree

docs/developer-guide.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,9 +1277,22 @@ interface YasrConfig {
12771277

12781278
// Error renderers
12791279
errorRenderers?: ErrorRenderer[];
1280+
1281+
// Optional callback enabling plugins to execute background SPARQL queries
1282+
executeQuery?: (query: string, options?: PluginQueryOptions) => Promise<any>;
1283+
}
1284+
1285+
interface PluginQueryOptions {
1286+
// Custom Accept header for the request (e.g. "text/turtle", "application/sparql-results+json")
1287+
acceptHeader?: string;
1288+
// Abort signal for cancelling in-flight background queries
1289+
signal?: AbortSignal;
12801290
}
12811291
```
12821292
1293+
When YASR is used inside a Yasgui `Tab`, the `executeQuery` callback is automatically
1294+
wired up. When using YASR standalone, you must provide your own implementation.
1295+
12831296
#### YASR Example
12841297
12851298
```javascript
@@ -2253,6 +2266,42 @@ const plugins = yasr.getPlugins();
22532266
console.log('Available plugins:', Object.keys(plugins));
22542267
```
22552268
2269+
##### `executeQuery(query: string, options?: PluginQueryOptions): Promise<any>`
2270+
2271+
Execute a background SPARQL query on behalf of a plugin. This delegates to the
2272+
`executeQuery` callback in the YASR configuration and returns the raw response
2273+
without replacing the currently displayed results.
2274+
2275+
The returned response object has the following shape:
2276+
2277+
```typescript
2278+
{
2279+
ok: boolean; // true if HTTP status is 2xx
2280+
status: number; // HTTP status code
2281+
statusText: string; // HTTP status text
2282+
headers: Headers; // Response headers
2283+
content: string; // Raw response body
2284+
data: string; // Alias for content
2285+
json(): Promise<any>; // Parse content as JSON
2286+
text(): Promise<string>; // Return content as string
2287+
}
2288+
```
2289+
2290+
Plugins can use this to fetch additional data (e.g. expanding a node in a graph
2291+
visualization) without interfering with the main query results.
2292+
2293+
```javascript
2294+
// Inside a plugin
2295+
const response = await this.yasr.executeQuery(
2296+
'DESCRIBE <http://example.org/resource>',
2297+
{ acceptHeader: 'text/turtle' }
2298+
);
2299+
const turtle = await response.text();
2300+
```
2301+
2302+
If no `executeQuery` callback is configured, the returned Promise rejects with
2303+
an error.
2304+
22562305
##### `download(filename?: string): void`
22572306
22582307
Download results using current plugin's download method.
@@ -2520,6 +2569,41 @@ interface Plugin<Options = any> {
25202569
download?(filename?: string): DownloadInfo | undefined;
25212570
}
25222571

2572+
#### Background Queries from Plugins
2573+
2574+
Plugins can execute additional SPARQL queries via `this.yasr.executeQuery()`
2575+
without overwriting the currently displayed results. This is useful for
2576+
interactive features like expanding a node in a graph visualization.
2577+
2578+
The query runs in **silent mode** — no YASQE lifecycle events (`queryBefore`,
2579+
`queryResponse`, etc.) are emitted and `yasr.setResponse()` is not called, so
2580+
the visible results remain unchanged.
2581+
2582+
```typescript
2583+
// Example: fetch additional data when a user interacts with a result
2584+
async expandNode(uri: string): Promise<void> {
2585+
const controller = new AbortController();
2586+
2587+
const response = await this.yasr.executeQuery(
2588+
`DESCRIBE <${uri}>`,
2589+
{
2590+
acceptHeader: 'text/turtle',
2591+
signal: controller.signal,
2592+
}
2593+
);
2594+
2595+
const body = await response.text();
2596+
// … parse and merge into the current visualization
2597+
}
2598+
```
2599+
2600+
**Response shape** — see [`executeQuery()` method](#executequeryquery-string-options-pluginqueryoptions-promiseany)
2601+
for the full response object description.
2602+
2603+
**Cancellation** — pass an `AbortSignal` via `options.signal` to cancel
2604+
in-flight requests (e.g. when the user navigates away or triggers a new
2605+
expansion before the previous one finishes).
2606+
25232607
interface DownloadInfo {
25242608
contentType: string;
25252609
getData: () => string;

package-lock.json

Lines changed: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
},
5353
"dependencies": {
5454
"@fortawesome/fontawesome-free": "^7.1.0",
55-
"@matdata/yasgui-graph-plugin": "^1.5.0",
55+
"@matdata/yasgui-graph-plugin": "^1.6.1",
5656
"@matdata/yasgui-table-plugin": "^1.3.0",
5757
"@typescript-eslint/eslint-plugin": "^6.13.2",
5858
"@typescript-eslint/parser": "^6.13.2",

packages/yasgui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"jsuri": "^1.3.1",
3434
"lodash-es": "^4.17.15",
3535
"sortablejs": "^1.10.2",
36-
"@matdata/yasgui-graph-plugin": "^1.4.1",
37-
"@matdata/yasgui-table-plugin": "^1.1.0"
36+
"@matdata/yasgui-graph-plugin": "^1.6.1",
37+
"@matdata/yasgui-table-plugin": "^1.3.0"
3838
},
3939
"devDependencies": {
4040
"@types/autosuggest-highlight": "^3.1.0",

packages/yasgui/src/Tab.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { addClass, removeClass, getAsValue } from "@matdata/yasgui-utils";
33
import { TabListEl } from "./TabElements";
44
import TabSettingsModal from "./TabSettingsModal";
55
import { default as Yasqe, RequestConfig, PlainRequestConfig, PartialConfig as YasqeConfig } from "@matdata/yasqe";
6-
import { default as Yasr, Parser, Config as YasrConfig, PersistentConfig as YasrPersistentConfig } from "@matdata/yasr";
6+
import {
7+
default as Yasr,
8+
Parser,
9+
Config as YasrConfig,
10+
PersistentConfig as YasrPersistentConfig,
11+
PluginQueryOptions as YasrPluginQueryOptions,
12+
} from "@matdata/yasr";
713
import { mapValues, eq, mergeWith, words, deburr, invert } from "lodash-es";
814
import * as shareLink from "./linkUtils";
915
import EndpointSelect from "./endpointSelect";
@@ -1584,6 +1590,7 @@ WHERE {
15841590
{
15851591
customQuery: query,
15861592
customAccept: "text/turtle",
1593+
silent: true,
15871594
},
15881595
);
15891596

@@ -1672,12 +1679,15 @@ WHERE {
16721679
// Add default renderers to the end, to give our custom ones priority.
16731680
...(Yasr.defaults.errorRenderers || []),
16741681
],
1675-
executeQuery: async (query: string, options?: { acceptHeader?: string }) => {
1682+
executeQuery: async (query: string, options?: YasrPluginQueryOptions) => {
16761683
if (!this.yasqe) throw new Error("No YASQE instance available");
1677-
return Yasqe.Sparql.executeQuery(this.yasqe, undefined, {
1684+
const response = await Yasqe.Sparql.executeQuery(this.yasqe, undefined, {
16781685
customQuery: query,
16791686
customAccept: options?.acceptHeader,
1687+
signal: options?.signal,
1688+
silent: true,
16801689
});
1690+
return response;
16811691
},
16821692
};
16831693
// Allow getDownloadFilName to be overwritten by the global config

packages/yasqe/src/sparql.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ export function getAjaxConfig(
174174
export interface ExecuteQueryOptions {
175175
customQuery?: string;
176176
customAccept?: string;
177+
/** Optional external abort signal, useful for plugin-driven background fetches. */
178+
signal?: AbortSignal;
179+
/**
180+
* Execute without emitting Yasqe query lifecycle events.
181+
* Useful for background/plugin-driven queries that should not update main UI state.
182+
*/
183+
silent?: boolean;
177184
}
178185

179186
export async function executeQuery(
@@ -182,13 +189,21 @@ export async function executeQuery(
182189
options?: ExecuteQueryOptions,
183190
): Promise<any> {
184191
const queryStart = Date.now();
192+
const silent = !!options?.silent;
185193
try {
186-
yasqe.emit("queryBefore", yasqe, config);
194+
if (!silent) yasqe.emit("queryBefore", yasqe, config);
187195
const populatedConfig = getAjaxConfig(yasqe, config);
188196
if (!populatedConfig) {
189197
return; // Nothing to query
190198
}
191199
const abortController = new AbortController();
200+
if (options?.signal) {
201+
if (options.signal.aborted) {
202+
abortController.abort();
203+
} else {
204+
options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
205+
}
206+
}
192207

193208
// Use custom accept header if provided, otherwise use the default
194209
const acceptHeader = options?.customAccept || populatedConfig.accept;
@@ -256,17 +271,22 @@ export async function executeQuery(
256271
populatedConfig.url = url.toString();
257272
}
258273
const request = new Request(populatedConfig.url, fetchOptions);
259-
yasqe.emit("query", request, abortController);
274+
if (!silent) yasqe.emit("query", request, abortController);
260275
const response = await fetch(request);
261276

262277
// Await the response content and merge with the `Response` object
278+
const content = await response.text();
263279
const queryResponse = {
264280
ok: response.ok,
265281
status: response.status,
266282
statusText: response.statusText,
267283
headers: response.headers,
268284
type: response.type,
269-
content: await response.text(),
285+
content,
286+
// Compatibility aliases for plugins that expect fetch-like or axios-like response objects.
287+
data: content,
288+
json: async () => JSON.parse(content),
289+
text: async () => content,
270290
};
271291

272292
if (!response.ok) {
@@ -278,8 +298,10 @@ export async function executeQuery(
278298
throw error;
279299
}
280300

281-
yasqe.emit("queryResponse", queryResponse, Date.now() - queryStart);
282-
yasqe.emit("queryResults", queryResponse.content, Date.now() - queryStart);
301+
if (!silent) {
302+
yasqe.emit("queryResponse", queryResponse, Date.now() - queryStart);
303+
yasqe.emit("queryResults", queryResponse.content, Date.now() - queryStart);
304+
}
283305
return queryResponse;
284306
} catch (e) {
285307
if (e instanceof Error && e.message === "Aborted") {
@@ -292,12 +314,12 @@ export async function executeQuery(
292314
if (e.message.includes("Failed to fetch") || e.message.includes("NetworkError")) {
293315
enhancedError.message = `${e.message}. The server may have returned an error response (check browser dev tools), but CORS headers are preventing JavaScript from accessing it. Ensure the endpoint returns proper CORS headers even for error responses (Access-Control-Allow-Origin, etc.).`;
294316
}
295-
yasqe.emit("queryResponse", enhancedError, Date.now() - queryStart);
317+
if (!silent) yasqe.emit("queryResponse", enhancedError, Date.now() - queryStart);
296318
} else {
297-
yasqe.emit("queryResponse", e, Date.now() - queryStart);
319+
if (!silent) yasqe.emit("queryResponse", e, Date.now() - queryStart);
298320
}
299321
}
300-
yasqe.emit("error", e);
322+
if (!silent) yasqe.emit("error", e);
301323
throw e;
302324
}
303325
}

packages/yasr/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@ export type Prefixes = { [prefixLabel: string]: string };
661661
export interface PluginQueryOptions {
662662
/** Optional custom Accept header for the request (e.g. "text/turtle"). */
663663
acceptHeader?: string;
664+
/** Optional abort signal for cancelling in-flight background queries. */
665+
signal?: AbortSignal;
664666
}
665667

666668
export interface PluginConfig {

0 commit comments

Comments
 (0)