Skip to content

Scout 3.1 Changes ​

Scout 3.1 trims the observation surface and aligns error naming with the rest of the monorepo.

tap() no longer emits dispose ​

The { type: 'dispose' } event duplicated disposed and disposalSignal. ScoutEvent<T> is now a single state-change shape. Observe disposal through the signal instead:

ts
// Before
search.tap((event) => {
  if (event.type === 'dispose') cleanup();
});

// After
search.disposalSignal.addEventListener('abort', cleanup);

ScoutConfigurationError is renamed ​

The config error is now ScoutConfigError, matching the <Pkg>ConfigError name used across the monorepo.

ts
// Before
import { ScoutConfigurationError } from '@vielzeug/scout';

// After
import { ScoutConfigError } from '@vielzeug/scout';

Scout 3 Changes ​

Scout 3 replaces per-property Ripple signals with one framework-neutral external store. This is a breaking change: use setQuery() for writes and read query, isSearching, and results together through getSnapshot().

Replace signal reads and writes ​

ts
// Before
search.query.value = 'alice';
console.log(search.isSearching.value, search.results.value);

// After
search.setQuery('alice');
const { isSearching, results } = search.getSnapshot();
console.log(isSearching, results);

Replace per-property subscriptions ​

ts
// Before
const stopResults = search.results.subscribe(render);
const stopSearching = search.isSearching.subscribe(render);

// After
const stop = search.subscribe(() => render(search.getSnapshot()));

One subscription now observes an atomic SearchSnapshot; subscribers never see isSearching: false paired with results from the previous committed query. Subscription errors are reported asynchronously after all subscribers run. Pass { signal } to detach automatically.

Bridge to Ripple explicitly ​

ts
import { createRipple } from '@vielzeug/ripple';

const ripple = createRipple();
const state = ripple.fromSubscribable(search, { signal: search.disposalSignal });
const names = ripple.computed(() => state.value.results.map((result) => result.item.name));

Scout no longer installs Ripple. Add @vielzeug/ripple directly only when your application uses this bridge.

Observe diagnostics with tap() ​

tap() emits typed state-change and dispose events. Tapper errors are swallowed so diagnostics cannot affect search behavior.

ts
const stop = search.tap((event) => {
  if (event.type === 'state-change') console.debug(event.snapshot);
});

Scout 2.4 Changes ​

Scout 2.4 removes the tap() observability layer from SearchState — it duplicated @vielzeug/ripple signal subscriptions that were already available via search.query.subscribe(), search.isSearching.subscribe(), and search.results.subscribe().

Removed exports:

  • SearchState.tap() method
  • ScoutEvent type

Replace tap() with signal subscriptions ​

ts
// Before
const stop = search.tap((event) => {
  if (event.type === 'query-change') console.debug('query:', event.query);
  if (event.type === 'results-change') console.debug('results:', event.results.length);
});

// After
const stopQuery = search.query.subscribe(() => console.debug('query:', search.query.peek()));
const stopResults = search.results.subscribe(() => console.debug('results:', search.results.peek().length));

Replace dispose event with disposalSignal ​

ts
// Before
search.tap((event) => { if (event.type === 'dispose') cleanup(); });

// After
search.disposalSignal.addEventListener('abort', () => cleanup());

Scout 2 Changes ​

Scout 2 adds setItems() for refreshed corpora, validates numeric search configuration, and replaces ScoutIndexError with ScoutConfigurationError, removes the unused ScoutError.is() type guard and exposes revision as a readonly property on the ScoutIndex interface.

Replace ScoutError.is() with instanceof ​

The static ScoutError.is() type guard is removed. Use instanceof ScoutError to narrow unknown values to the Scout error hierarchy.

ts
// Scout 2
if (ScoutError.is(err)) { ... }

// Scout 3
if (err instanceof ScoutError) { ... }

Use index.revision for cache invalidation ​

The internal _index-state side-channel is removed. ScoutIndex now exposes revision as a readonly property — a monotonically increasing counter incremented after every changed mutation. Use it directly when caching search results outside the index.

ts
// Scout 2 — internal side-channel (no public API)
// toSearchMatcher() used a private WeakMap to track index revisions

// Scout 3 — public readonly property
const revision = index.revision;

toSearchMatcher() now reads index.revision directly. External code that cached search results can use the same property.

Removed export:

  • ScoutIndexError

Added export:

  • ScoutConfigurationError

Replace ScoutIndexError ​

ts
// Scout 1
import { ScoutIndexError } from '@vielzeug/scout';
ts
// Scout 2
import { ScoutConfigurationError } from '@vielzeug/scout';

ScoutConfigurationError covers invalid index fields, search constraints, and reactive debounce settings.

Fix Invalid Numeric Options ​

Scout 1 accepted several invalid numeric values. Scout 2 rejects them with ScoutConfigurationError.

OptionScout 2 domain
field weightfinite, greater than 0
thresholdfinite, 0..1
limitfinite non-negative integer
minQueryLengthfinite positive integer
debouncefinite non-negative integer
ts
// Scout 1: negative limit returned no results
index.search('alice', { limit: -1 });

// Scout 2: use zero to request no results
index.search('alice', { limit: 0 });

Reconcile Refreshed Corpora ​

Use setItems() instead of coordinating items, add(), remove(), and reindex() yourself.

ts
// Scout 1
const existing = new Set(index.items);
const incoming = new Set(latestUsers);

for (const item of existing) {
  if (!incoming.has(item)) index.remove(item);
}

for (const item of latestUsers) {
  if (!existing.has(item)) index.add(item);
  else index.reindex(item);
}
ts
// Scout 2
index.setItems(latestUsers);

setItems() uses reference identity, reindexes retained items, adopts incoming first-occurrence order, collapses duplicate references, and emits one mutation notification only when indexed values, membership, or order changes.

Highlighting ​

findMatchRanges(text, query) now accepts raw query text and normalizes punctuation and whitespace with Scout's tokenizer. SearchResult.matches stays unchanged: it contains literal normalized-token ranges and can be empty for a fuzzy-only candidate.