Skip to content

feat(api): thread AbortSignal through querySparql wrappers#606

Open
HexaField wants to merge 1 commit into
devfrom
feat/query-abort
Open

feat(api): thread AbortSignal through querySparql wrappers#606
HexaField wants to merge 1 commit into
devfrom
feat/query-abort

Conversation

@HexaField

@HexaField HexaField commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Threads an optional AbortController.signal through every public API method in packages/api/ that wraps a querySparql call. Callers can now cancel long-running queries (timeline loads, embedding fetches, topic lookups) when a view unmounts or a newer query supersedes the in-flight one — the executor short-circuits the JSON reply and the wrappers re-throw the cancellation as DOMException('Aborted', 'AbortError') instead of swallowing it.

Backwards compatible — every new parameter is optional.

Why

Long-running SPARQL queries (think Channel.allItems over a chatty channel, or SemanticRelationship.allItemEmbeddings while embeddings are still being indexed) can ship megabytes of JSON back over the WebSocket. Without cancellation, a Synergy view that unmounts mid-fetch still pays the serialise + transit + deserialise tax, even though nothing renders the result. Now the wrappers honour an AbortSignal and forward it to the executor.

Caveat: Oxigraph itself can't be interrupted mid-evaluation, so the blocking thread keeps grinding until the query returns; what's saved is the JSON serialise + WebSocket reply + client deserialise tax, which is the dominant cost for large result sets.

Wire protocol

Built on the executor-side support shipped in coasys/ad4m#855:

// client → server, when AbortController fires
{ "id": "<cancel-msg-id>",
  "type": "request.cancel",
  "params": { "targetId": "<original-request-id>" } }
// the cancelled call replies with
{ "id": "<original-request-id>",
  "error": { "code": 499, "message": "Request cancelled by client" } }

The CallOptions shape is structurally compatible with ad4m's — Flux uses a local AbortOptions { signal?: AbortSignal } interface (in packages/api/src/shared/abort.ts, re-exported from the package root) so it doesn't need an ad4m version bump to consume the new executor surface. Anyone consuming the SDK can already pass { signal } if they're running against an executor with cancellation support; against older executors the signal is harmlessly forwarded and ignored.

Methods wired

Wrapper Methods
Channel allItems, unprocessedItems, totalItemCount, recentConversations, pinnedConversations
Conversation stats, topics, subgroups, subgroupsData
ConversationSubgroup stats, topics, itemsData, topicsWithRelevance
SemanticRelationship itemEmbedding, allConversationEmbeddings, allSubgroupEmbeddings, allItemEmbeddings, allItemEmbeddingsByType
Topic linkedConversations, linkedSubgroups

Each wrapper:

  1. Accepts options?: AbortOptions as the last parameter.
  2. Forwards options to every perspective.querySparql<T>(query, options) (and perspective.get(query, options) where applicable) inside the method body.
  3. Re-throws DOMException('Aborted', 'AbortError') instead of swallowing it — so callers can distinguish cancellation from a real query failure. Other errors are still logged + a sensible default returned (matches pre-existing behaviour).

conversation/util.ts is an internal helper called only by removeEmbedding; left alone to keep the diff focused on user-facing API.

Reviewer workflow

To verify end-to-end against the companion ad4m PR, use the existing branch-aware build script:

# From the Flux checkout root:
BRANCH=feat/query-abort scripts/build-with-ad4m-link.sh

That script detects the branch from BRANCH (or CI env vars), checks whether coasys/ad4m has a matching branch, clones it, builds ad4m/core + ad4m/connect + hooks, rewrites Flux's pnpm.overrides to file:./ad4m/..., clears all caches, and builds Flux.

The companion WE PR coasys/we#72 adds a developer-facing equivalent at scripts/sync-ad4m-branch.sh with explicit --ad4m and --branch flags for local dev outside CI.

Test plan

  • Channel.allItems forwards the signal to perspective.querySparql.
  • Channel.allItems re-throws DOMException('Aborted', 'AbortError').
  • All existing wrapper tests still pass (114/114 in packages/api vitest suite).
  • Manual: hit a slow Synergy channel, navigate away mid-load, confirm the in-flight request.cancel is sent (DevTools → Network → WS frames) — easiest via BRANCH=feat/query-abort scripts/build-with-ad4m-link.sh to pick up the companion executor support.
  • CI green.

Coordination

  • Companion ad4m PR: coasys/ad4m#855 — adds executor + Ad4mModel.findAll/modelQuery signal threading.
  • Companion WE PR: coasys/we#72 — wires the signal through SchemaRenderer and ships a scripts/sync-ad4m-branch.sh for the same workflow.
  • All three PRs share the branch name feat/query-abort.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • API methods now support optional request cancellation parameters
    • Enhanced error handling for cancelled requests across data-fetching operations
  • Tests

    • Added test coverage for abort signal forwarding and error propagation

Adds opt-in cancellation to every public API method that wraps a
querySparql call.  Callers pass `{ signal }` from an AbortController and
the executor receives a `request.cancel` over the WebSocket, short-
circuiting the JSON reply.  Aborted calls reject with `DOMException
('Aborted', 'AbortError')` — wrappers re-throw this so callers can
distinguish cancellation from real failures (non-AbortError exceptions
are still logged + swallowed for graceful degradation).

Wired through:
- Channel: allItems, unprocessedItems, totalItemCount,
  recentConversations, pinnedConversations
- Conversation: stats, topics, subgroups, subgroupsData
- ConversationSubgroup: stats, topics, itemsData, topicsWithRelevance
- SemanticRelationship: itemEmbedding, allConversationEmbeddings,
  allSubgroupEmbeddings, allItemEmbeddings, allItemEmbeddingsByType
- Topic: linkedConversations, linkedSubgroups

Structural `AbortOptions { signal?: AbortSignal }` type lives in
`packages/api/src/shared/abort.ts` and is re-exported from the package
index.  Structurally compatible with ad4m's `CallOptions` — no version
bump required to consume the underlying executor cancellation support
shipped in coasys/ad4m#855.

Tests
- Forwarding: Channel.allItems passes the signal to
  perspective.querySparql.
- Re-throw: Channel.allItems propagates DOMException AbortError instead
  of swallowing it.

Backwards compatible: all new parameters are optional, existing call
sites are unchanged.

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

netlify Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit 0588cc9
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/6a273f9a1d5b0b0008e4186f
😎 Deploy Preview https://deploy-preview-606--fluxsocial-dev.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.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds cooperative request cancellation support across the Flux API package by introducing an AbortOptions type and systematically propagating it through query methods in Channel, Conversation, ConversationSubgroup, SemanticRelationship, and Topic models. Each method now rethrows AbortError exceptions instead of swallowing them.

Changes

Request Cancellation Pattern Across API Models

Layer / File(s) Summary
Abort cancellation contract
packages/api/src/shared/abort.ts, packages/api/src/index.ts
New AbortOptions interface with optional signal?: AbortSignal field is defined and re-exported as part of the public API surface.
Channel query methods with cancellation
packages/api/src/channel/index.ts, packages/api/src/channel/channel.test.ts
Five Channel methods (allItems, unprocessedItems, totalItemCount, recentConversations, pinnedConversations) now accept options?: AbortOptions, thread it through underlying perspective calls, and explicitly rethrow AbortError while preserving fallback behavior. Test cases verify signal forwarding and error propagation.
Conversation query methods with cancellation
packages/api/src/conversation/index.ts
Four Conversation methods (stats, topics, subgroups, subgroupsData) accept AbortOptions, forward it through both direct SPARQL queries and transitive ConversationSubgroup.findAll() calls, and rethrow AbortError.
ConversationSubgroup query methods with cancellation
packages/api/src/conversation-subgroup/index.ts
Four ConversationSubgroup methods (stats, topics, itemsData, topicsWithRelevance) accept AbortOptions and forward it into SPARQL queries with explicit AbortError rethrow behavior.
SemanticRelationship embedding queries with cancellation
packages/api/src/semantic-relationship/index.ts
Five embedding-fetching methods (itemEmbedding, allConversationEmbeddings, allSubgroupEmbeddings, allItemEmbeddings, allItemEmbeddingsByType) accept AbortOptions, pass it to perspective.querySparql(), and rethrow AbortError on abort while returning fallbacks for other errors.
Topic relationship query methods with cancellation
packages/api/src/topic/index.ts
Two Topic methods (linkedConversations, linkedSubgroups) accept AbortOptions, forward it to SPARQL queries, and rethrow AbortError with existing fallback handling preserved.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • coasys/flux#585: Refactors the same code paths (Channel.unprocessedItems(), Conversation.subgroupsData()) with SPARQL query logic changes that this PR layers cancellation support onto.
  • coasys/flux#526: Rewrites Prolog→SurrealQL query logic in the same data-fetching methods that this PR adds AbortOptions propagation to.

Suggested reviewers

  • lucksus
  • jhweir

Poem

🐰 A signal to abort, so clear and so bright,
Through queries and conversations, we thread cancellation right.
From Channel to Topic, the pattern runs deep—
Rethrow the AbortError, no promises to keep!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(api): thread AbortSignal through querySparql wrappers' accurately and specifically describes the main change: adding AbortSignal support throughout the API wrapper methods.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/query-abort

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/conversation/index.ts`:
- Around line 135-141: The call in Conversation.subgroups is passing
AbortOptions as a third arg to ConversationSubgroup.findAll which the
mocked/real API treats as a single options object; fix by passing the
abort/cancellation options inside the same options object passed as the second
parameter—i.e. call ConversationSubgroup.findAll(this.perspective, { parent: {
model: Conversation, id: this.id }, ...(options ?? {}) }) so the abort signal is
propagated; update the subgroups method accordingly (referencing
ConversationSubgroup.findAll and the subgroups method).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 167606f0-efdd-46a0-8e7d-2160a4cef550

📥 Commits

Reviewing files that changed from the base of the PR and between bc3bae2 and 0588cc9.

📒 Files selected for processing (8)
  • packages/api/src/channel/channel.test.ts
  • packages/api/src/channel/index.ts
  • packages/api/src/conversation-subgroup/index.ts
  • packages/api/src/conversation/index.ts
  • packages/api/src/index.ts
  • packages/api/src/semantic-relationship/index.ts
  • packages/api/src/shared/abort.ts
  • packages/api/src/topic/index.ts

Comment on lines +135 to 141
async subgroups(options?: AbortOptions): Promise<ConversationSubgroup[]> {
// find the conversations subgroup entities — use parent-scoped query
// instead of this.get() which fetches all Conversation triples
return ConversationSubgroup.findAll(this.perspective, {
parent: { model: Conversation, id: this.id },
}) as unknown as Promise<ConversationSubgroup[]>;
}, options) as unknown as Promise<ConversationSubgroup[]>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the findAll method definition in Ad4mModel or its implementation
# to confirm the signature accepts options as third parameter

# Check local Ad4m model implementations
rg -n -A 10 'static\s+(async\s+)?findAll' --type=ts

# Check if there's any documentation or type definition for findAll
rg -n -B 2 -A 5 'findAll.*perspective.*where' --type=ts

Repository: coasys/flux

Length of output: 3622


Fix ConversationSubgroup.findAll options placement (AbortOptions as 3rd arg likely ignored)

  • In packages/api/src/conversation/conversation.test.ts, findAll is mocked as static async findAll(perspective, opts?) (2 params). The call in packages/api/src/conversation/index.ts passes AbortOptions as a 3rd argument (this.perspective, { parent: ... }, options), which won’t match that API and will likely not propagate abort/cancellation.
  • Adjust the ConversationSubgroup.findAll call to pass AbortOptions in the parameter position actually supported by the real Ad4mModel.findAll implementation (or through its expected cancellation mechanism).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/conversation/index.ts` around lines 135 - 141, The call in
Conversation.subgroups is passing AbortOptions as a third arg to
ConversationSubgroup.findAll which the mocked/real API treats as a single
options object; fix by passing the abort/cancellation options inside the same
options object passed as the second parameter—i.e. call
ConversationSubgroup.findAll(this.perspective, { parent: { model: Conversation,
id: this.id }, ...(options ?? {}) }) so the abort signal is propagated; update
the subgroups method accordingly (referencing ConversationSubgroup.findAll and
the subgroups method).

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