Skip to content

feat(schema-renderer): abort in-flight findAll on cleanup and re-run#71

Closed
HexaField wants to merge 1 commit into
coasys:devfrom
HexaField:feat/query-abort
Closed

feat(schema-renderer): abort in-flight findAll on cleanup and re-run#71
HexaField wants to merge 1 commit into
coasys:devfrom
HexaField:feat/query-abort

Conversation

@HexaField

Copy link
Copy Markdown
Contributor

Summary

Wires an AbortController into SchemaRenderer so the in-flight Ad4mModel.findAll is cancelled the moment the rendering effect re-runs (perspective swap, params change) or the component unmounts.

Why it matters

createQuerySignal and the $single path both run findAll inside createEffect. Without a signal, every reactive dep change leaks an in-flight query that keeps grinding through SPARQL eval → JSON serialise → WebSocket reply → client deserialise, all of which is discarded the moment the new result arrives. On chatty perspectives that adds up fast — a perspective swap can leave 4–5 dead queries shipping megabytes of JSON before the new one even starts.

How

  • const controller = new AbortController() inside the effect.
  • onCleanup(() => controller.abort()) — Solid fires this before the effect re-runs and on unmount.
  • ModelClass.findAll(p, queryOptions, { signal: controller.signal }) — the structural { signal } shape matches Ad4mModel.findAll's new third argument added in coasys/ad4m#855, which forwards the signal through modelQueryapiClient.call → the executor's request.cancel WebSocket message.
  • .then guards on controller.signal.aborted so a stale result that arrives after cancellation doesn't overwrite the new effect's state.
  • .catch swallows DOMException('Aborted', 'AbortError') so cancellation isn't surfaced as a UI error — the new effect run (or unmount) handles state.

The subscribe path already cleans up via builder.dispose(); not changed.

Caveat

The executor's Oxigraph SPARQL engine has no internal interrupt hook, so the blocking thread keeps running until the query returns. What's saved is the JSON serialise + WebSocket reply + client deserialise tax, which is the dominant cost for any non-trivial result set. The API is forward-compatible with a future Oxigraph interrupt.

Files changed

  • packages/schema-system/frameworks/solid/src/SchemaRenderer.tsx — two findAll call sites (createQuerySignal list path + $single one-shot path) now scope an AbortController per effect iteration.
  • packages/schema-system/frameworks/solid/tests/queryToken.test.tsx — 3 new tests for the new behaviour.

Test plan

  • findAll receives an AbortSignal in options.signal.
  • Unmount aborts the controller (signal goes from aborted: falsetrue).
  • Effect re-run (perspective signal change) aborts the prior controller and gives the new run a fresh signal.
  • AbortError rejection is swallowed without surfacing.
  • Local install hit Node OOM in this workspace; CI on the upstream branch is the verification path.

Coordination

  • Same-named branch in coasys/ad4m extends the executor + Ad4mModel.findAll/modelQuery to thread the signal.
  • Same-named branch in coasys/flux wires it through the API wrappers.
  • Until the matching ad4m version lands, the signal is harmlessly forwarded and ignored by older Ad4mModel.findAll (3rd arg is dropped) — no breakage, just no early termination.

🤖 Generated with Claude Code

SchemaRenderer's `createQuerySignal` and `$single` paths both run
`ModelClass.findAll` inside a `createEffect`.  The effect re-runs on any
reactive dependency change (perspective swap, params token update) and
the component can unmount mid-query.  Without cancellation the stale
findAll keeps grinding through SPARQL serialisation, network round-trip,
and client-side deserialisation — work that's discarded the moment a
fresher result arrives.

Wires an `AbortController` scoped to each effect iteration:

  - `onCleanup` aborts the controller before the effect re-runs or the
    component unmounts.
  - The `findAll(p, queryOptions, { signal })` call forwards the signal
    to ad4m's `Ad4mModel.findAll` (extended in
    coasys/ad4m#855 to thread the signal through
    `modelQuery` → `apiClient.call` → the executor's `request.cancel`
    WebSocket message).
  - `.then` guards on `controller.signal.aborted` to ignore the stale
    result if the controller fired before the promise resolved.
  - `.catch` swallows `DOMException('Aborted', 'AbortError')` so
    cancellation isn't surfaced to the UI as an error — the new effect
    run (or unmount) handles state instead.

The subscribe path already cleans up via `builder.dispose()` and isn't
changed.

Caveat: the executor's Oxigraph SPARQL engine can't be interrupted
mid-evaluation; what's saved is the JSON serialise + WebSocket reply +
client deserialise tax, which dominates for any non-trivial result set.

Tests (queryToken.test.tsx):
- findAll receives an AbortSignal in `options.signal`.
- Unmount aborts the controller.
- Re-running the effect (perspective signal change) aborts the prior
  controller and gives the new run a fresh, un-aborted signal.
- AbortError rejection is swallowed without surfacing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@netlify

netlify Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploy Preview for coasys-we ready!

Name Link
🔨 Latest commit c6ce71d
🔍 Latest deploy log https://app.netlify.com/projects/coasys-we/deploys/6a27419e790ce80009d6f312
😎 Deploy Preview https://deploy-preview-71--coasys-we.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@HexaField

Copy link
Copy Markdown
Contributor Author

Re-opening from coasys/we branch directly (not from fork) — see successor PR.

@HexaField HexaField closed this Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant