Skip to content

fix(frameworks): type scanner entrypoints and cancel scans on disposal - #256

Merged
truthixify merged 3 commits into
wraith-protocol:developfrom
Moonwalker-rgb:fix/framework-scanner-typing-cancellation
Oct 17, 2026
Merged

truthixify merged 3 commits into
wraith-protocol:developfrom
Moonwalker-rgb:fix/framework-scanner-typing-cancellation

Conversation

@Moonwalker-rgb

@Moonwalker-rgb Moonwalker-rgb commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #243
Closes #244
Closes #245

The four framework adapters — @wraith-protocol/sdk-react, @wraith-protocol/sdk-vue, @wraith-protocol/sdk-svelte and @wraith-protocol/sdk-solid — had drifted away from the typed SDK exports, and none of them cancelled work when their consumer went away. This PR replaces the casts with SDK types, makes the Vue scanner options chain-discriminated, and gives all four adapters one cancellation contract with a stale-result guarantee.

  • sdk-react — no any left in the network hooks; typed scanner options, typed Horizon balances, shared error narrowing, and abort on unmount / dependency change.
  • sdk-vue — scanner and activity inputs are a discriminated union by chain; mismatches are compile-time errors in TypeScript and TypeErrors at runtime for JavaScript consumers.
  • sdk-svelte / sdk-solid — same cancellation shape, abort on owner disposal, superseded results discarded.
  • Prerequisite fix — the framework agent hooks pointed at a root export that no longer exists, which failed type-checking and sdk-vue's declaration build; they now use @wraith-protocol/sdk-agent, and the SSR compatibility check imports and renders all three packages instead of xfailing them.

#243 — Remove unsafe any from sdk-react network hooks

Done when (from the issue):

Criterion Implementation Test
Use SDK chain unions and typed Horizon response shapes UseScannerOptions.chain is 'stellar' (React's scanner reads Stellar announcements only); balances are read through Horizon.HorizonApi.BalanceLine instead of (b: any) packages/sdk-react/test/useScanner.test-d.ts
Narrow caught values through shared error helpers getUnknownErrorMessage() and isAbortError() are exported from the package root and used in hooks.ts, hooks/useScanner.ts hooks.test.tsx, useScanner.test.tsx
Add compile-time tests for invalid hook options packages/sdk-react/test/useScanner.test-d.ts runs under pnpm test:types
// before
const g = globalThis as any;                                  // shared cache global
acc.balances.find((b: any) => b.asset_type === 'native');     // Horizon response
for await (const ann of fetchAnnouncementsStream(chain, fetchOpts as any)) { … }
} catch (err: any) { setError(err); }                         // caught value

// after
type CacheGlobal = typeof globalThis & { __wraith_idb_cache?: IDBCache };
const xlmBalance = acc.balances.find((balance) => balance.asset_type === 'native');
for await (const announcement of fetchAnnouncementsStream(chain, fetchOpts)) { … }
} catch (err) {
  setError(err instanceof Error ? err : new Error(getUnknownErrorMessage(err)));
}

Compile-time tests reject unsupported chains, non-string wallets, non-numeric fromLedger and non-AbortSignal signals:

// @ts-expect-error - the React scanner only reads Stellar announcements
useScanner({ chain: 'evm' });

// @ts-expect-error - signal must be an AbortSignal
void result.scan({ signal: 'stop' });

Two behaviour fixes came out of this work:

  1. useStellarName defaulted to chain = 'testnet', but the only Stellar deployment key is 'stellar', so getDeployment() threw on every default call. The default is now 'stellar'.
  2. IDBCache's in-memory fallback (used whenever indexedDB is unavailable, including SSR) had a commit() copied from the IndexedDB path that referenced this.db, this.storeName and this.fallback — none of which exist on MemoryCache. It threw at runtime and failed tsc; it now delegates to set() and advances the watermark.

#244 — Make sdk-vue scanner and activity keys chain-discriminated

Done when (from the issue):

Criterion Implementation Test
Model options as a discriminated union by chain ScannerFetchOptions = EvmFetchOptions | StellarFetchOptions | SolanaFetchOptions; ScannerScanInput, ScannerScanWithKeysInput, ScannerCheckInput are unions over EvmStealthKeys / StellarStealthKeys / SolanaStealthKeys packages/sdk-vue/test/scanner.test-d.ts
Reject mismatched key material at compile time Chain-specific overloads on scanAnnouncements, checkAddress, scanWithKeys scanner.test-d.ts (@ts-expect-error cases)
Keep runtime validation for JavaScript consumers assertScannerChain, assertScannerFetchOptions, assertScannerScanInput, assertScannerScanWithKeysInput, assertScannerCheckInput useScanner.test.ts, useActivity.test.ts
// before - the key layout was guessed from the composable's active chain
scanner.scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingKey);
await scanner.fetchAnnouncements('horizen');
await scanner.scanWithKeys(keys);

// after
scanner.scanAnnouncements({ chain: 'evm', announcements, keys });
await scanner.fetchAnnouncements({ chain: 'evm', network: 'horizen' });
await scanner.scanWithKeys({ chain: 'evm', keys });

Mismatches are rejected in both directions:

// @ts-expect-error - Stellar key material cannot scan an EVM announcement list
scanner.scanAnnouncements({ chain: 'evm', announcements: evmAnnouncements, keys: stellarKeys });
expect(() =>
  composable.scanAnnouncements({
    chain: 'evm',
    announcements: evmAnnouncements,
    keys: stellarKeys, // JavaScript caller, no compiler help
  }),
).toThrow(TypeError); // "EVM scanner keys must provide a 0x-prefixed hex string \"viewingKey\""

useActivity().scanAnnouncements() takes the same { chain, announcements, keys } union, so activity and scanner inputs cannot disagree about a chain, and the as any[] / Record<string, string> casts are gone.

Breaking change on 0.1.x (called out in the changelog): positional arguments are replaced by options objects because the positional signature could not express the chain/key relationship.


#245 — Make framework scanner cancellation consistent

Done when (from the issue):

Criterion Implementation Test
One cancellation option shape across each framework's scan APIs Root export ScanCancellationOptions ({ signal?: AbortSignal }), accepted by React's hook and scan, Vue's fetch inputs, Svelte's factory and scan, and Solid's factory and scan all four scanner suites
Abort in-flight work on component disposal or dependency change React: unmount + chain/wallet change; Vue: onScopeDispose; Solid: onCleanup; Svelte: factory signal + dispose(); all four abort the previous scan on restart useScanner.test.tsx, useScanner.test.ts, useStellarAnnouncementScan.test.ts, createScanner.test.ts
Test that stale results cannot update unmounted consumers Unmount/scope/root disposal tests plus a "source ignores the abort" test per adapter as above
// one shape everywhere
interface ScanCancellationOptions {
  signal?: AbortSignal;
}

Cancellation semantics:

  • Aborted scans reject with the standard AbortError (DOMException).
  • A cancellation never sets the adapter's error state, so the UI does not flash a failure when a user navigates away.
  • Every adapter stamps each scan and checks the stamp, the disposal flag and the abort signal before publishing, so a superseded scan cannot overwrite newer state — even when the underlying source ignores the signal, which is the case for the EVM and Solana fetchers today.
  • Aborting a newer scan does not clear results an earlier scan already published.

The dedicated test for the last point starts two scans, releases the second, then releases the first, and asserts the published list is still the second scan's result:

releases[1]();
await second;
expect(composable.announcements.value).toEqual([{ ...announcement, ledger: 2 }]);

releases[0]();                    // first scan finally resolves
const outcome = await first;
expect((outcome as DOMException).name).toBe('AbortError');
expect(composable.announcements.value).toEqual([{ ...announcement, ledger: 2 }]);

Where cancellation actually reaches the network

Chain Behaviour
Stellar signal is passed into FetchAnnouncementsOptions, so in-flight Soroban RPC and Horizon requests abort (src/chains/stellar/announcements.ts).
EVM fetchAnnouncements() in the core has no signal parameter yet; the Vue adapter races the promise against the signal. An aborted fetch rejects immediately and cannot publish; the HTTP request completes in the background.
Solana Same as EVM.

The caller-visible guarantee is identical for all three chains.


Prerequisite: framework packages now import the agent client from its own package

Wraith, WraithAgent, Chain and the agent config types moved out of the core package in #193, but useWraith (Vue, Svelte) and createActivity (Solid) still imported them from @wraith-protocol/sdk. Their unit tests passed because the module was mocked, which hid the problem from everything except tsc and the declaration build:

src/composables/useWraith.ts(3,3): error TS2305: Module '"@wraith-protocol/sdk"' has no exported member 'Wraith'.

The imports, test mocks, package dependencies, tsup externals and tsconfig paths now point at @wraith-protocol/sdk-agent. The compatibility harness previously listed all three packages under KNOWN_IMPORT_FAILURES ("Node refuses to link them"); it now imports and server-renders them, so those entries are removed rather than skipped.


Public API additions

Export From Why
ScanCancellationOptions @wraith-protocol/sdk One cancellation shape for every framework adapter
getUnknownErrorMessage(error: unknown): string @wraith-protocol/sdk Shared narrowing for non-Error throws
isAbortError(error: unknown): boolean @wraith-protocol/sdk Distinguish cancellation from failure

New Vue exports: ScannerChain, ScannerFetchOptions, ScannerScanInput, ScannerScanWithKeysInput, ScannerCheckInput, EvmScanInput, StellarScanInput, SolanaScanInput, ScanActivityInput, the five assert* guards. etc/sdk.api.md is regenerated.


Files worth reading first

File What changed
src/scanner/unified.ts ScanCancellationOptions
src/chains/errorUtils.ts isAbortError()
packages/sdk-react/src/hooks/useScanner.ts Typed cache global, abort controller, stale guard
packages/sdk-vue/src/composables/useScanner.ts Discriminated options, overloads, runtime guards, runCancellable()
packages/sdk-vue/src/composables/useActivity.ts Same union, no casts
packages/sdk-svelte/src/primitives/useStellarAnnouncementScan.ts Signal, dispose(), stale guard
packages/sdk-solid/src/primitives/createScanner.ts onCleanup(dispose), signal, stale guard
docs/guides/framework-scanner-cancellation.mdx Reference guide: lifecycle table, semantics, per-chain cancellation, migration
scripts/compat/verify-ssr-imports.mjs Drops the three known import failures

Testing

New test files and what they prove:

  • packages/sdk-react/test/useScanner.test.tsx — publish, unmount abort, superseded scan whose source ignores the abort, per-call signal, hook-level signal.
  • packages/sdk-react/test/useScanner.test-d.ts — compile-time rejection of unsupported chains, non-string wallets and malformed scan options.
  • packages/sdk-vue/test/useScanner.test.ts — discriminated fetch/scan/check inputs, runtime TypeError for mismatched keys and unknown chains, effect-scope disposal, caller signal, superseded scan.
  • packages/sdk-vue/test/scanner.test-d.ts — compile-time rejection of mismatched keys, unknown chains, wrong option fields per chain.
  • packages/sdk-vue/test/useActivity.test.ts — same union for activity, runtime rejection of mismatched keys.
  • packages/sdk-svelte/test/useStellarAnnouncementScan.test.ts — dispose, caller signal, factory signal, superseded scan.
  • packages/sdk-solid/test/createScanner.test.ts — root disposal via createRoot, caller signal, superseded scan.
Command Result
pnpm format:check pass
pnpm build pass (all 7 entry points)
pnpm test:types pass, includes both new *.test-d.ts suites
pnpm api:check pass (etc/sdk.api.md updated)
pnpm test 1555 root tests + 15 svelte tests pass
pnpm test:exports pass
pnpm test:compat pass — Node 20/22/24, Bun, browser bundles, optional peers, SSR imports
pnpm size pass, no limit regressions
pnpm --filter @wraith-protocol/sdk-{react,vue,solid,svelte} build + typechecks pass (these previously failed with TS2305)

CI on this pull request: 23 checks, 21 pass, 2 nightly jobs skipped, 0 failures.

Reviewer notes

  • The Vue API change is the only breaking change; it is documented in the changelog (### Changed) and with a before/after in the new guide.
  • The stale-result guard is deliberately independent of the abort signal, because the EVM and Solana fetchers ignore it today. If those fetchers gain signal support later, the guard stays correct and the race in the adapter can be deleted.
  • useScanner's hook-level signal aborts in-flight work but does not permanently dispose the hook; pass the signal to scan() if a scan should never outlive a specific call.
  • docs/guides/framework-scanner-cancellation.mdx mirrors the implementation; if the semantics change, update it in the same PR.

Framework adapters drifted from the SDK's typed exports: sdk-react cast
Horizon balances, scanner options, the shared cache global and caught errors
to `any`, sdk-vue selected chain URLs and key fields through repeated `as any`,
and none of the four adapters aborted work when its consumer was disposed.

- Export `ScanCancellationOptions`, `getUnknownErrorMessage` and
  `isAbortError` from the package root so React, Vue, Svelte and Solid share
  one cancellation shape and one way to narrow caught values.
- sdk-react: type the scanner cache global, read Horizon balances through
  `Horizon.HorizonApi.BalanceLine`, drop the remaining casts, narrow caught
  values, and abort on unmount and dependency changes so stale results cannot
  reach published state. Also fix `IDBCache`'s in-memory `commit()`, which
  referenced IndexedDB-only fields and threw without `indexedDB`.
- sdk-vue: model scanner options as a discriminated union by chain, pair each
  chain with its own announcement and key types, keep runtime `TypeError`
  guards for JavaScript consumers, and abort on effect scope disposal.
- sdk-svelte and sdk-solid: accept the same signal, abort on `dispose()` or
  owner cleanup, and discard superseded results.
- Add compile-time tests for invalid scanner options and runtime tests for
  unmount/scope/root disposal and stale results across all four adapters.
- Point the framework agent hooks at `@wraith-protocol/sdk-agent` and
  type-check the framework packages in CI.

Closes wraith-protocol#243
Closes wraith-protocol#244
Closes wraith-protocol#245
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@Moonwalker-rgb Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

The SSR compatibility harness listed sdk-solid, sdk-svelte and sdk-vue as
known import failures because they still pulled `Wraith`, `WraithAgent` and
`Chain` from the core package after the agent client moved to
`@wraith-protocol/sdk-agent`. They now import the agent client from its own
package, so the harness failed with "imports cleanly now; remove it from
KNOWN_IMPORT_FAILURES".

Drop the entries and let the check import and render all three companions
server-side.
The four framework adapters now share one cancellation option shape and a
stale-result guarantee that callers have to reason about, and the Vue scanner
API changed shape. Add a guide covering the per-framework lifecycle, the
abort semantics (AbortError, no error state, superseded results), which chain
requests actually cancel, the chain-discriminated Vue options and the
positional-argument migration, and link it from the README and changelog.

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Framework scanner cancellation passes the focused React, Vue, Solid, and core build checks. Approved.

@truthixify
truthixify merged commit 8c19dff into wraith-protocol:develop Oct 17, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants