@equinor/fusion-query

中文文档
7.0.8发布于 2026/11/12ISCTypeScriptES Module近 30 天下载 1.3万

Reactive data fetching and caching library with observable streams and comprehensive event system

原始使用文档
文档目录25 节
  1. When to Use
  2. Installation
  3. Quick Start
  4. Core Concepts
  5. Query
  6. QueryClient
  7. QueryCache
  8. QueryTask
  9. Configuration Options
  10. Queue Strategies
  11. Cache Management
  12. Mutation (Optimistic Update)
  13. Invalidation
  14. Custom Cache Validation
  15. Shared Cache
  16. Persistent Query
  17. Event System
  18. Query Events
  19. QueryClient Events
  20. QueryCache Events
  21. Subscribing to Events
  22. React Integration
  23. Operators
  24. Cleanup
  25. Sub-path Exports

@equinor/fusion-query

Reactive data fetching and caching library built on RxJS Observables with queue strategies, automatic retry, and a comprehensive event system.

When to Use

Use @equinor/fusion-query when you need to:

  • Fetch remote data with built-in caching that prevents redundant network requests
  • Manage concurrent requests with configurable queue strategies (switch, merge, concat)
  • Automatically retry failed requests with customizable delay and count
  • React to query lifecycle events for logging, debugging, or telemetry
  • Share cached data across multiple consumers through a shared QueryCache
  • Perform optimistic updates and cache mutations

Installation

pnpm add @equinor/fusion-query

Quick Start

import { Query } from '@equinor/fusion-query';

interface User {
  id: string;
  name: string;
}

const userQuery = new Query<User, { id: string }>({
  client: {
    fn: async (args, signal) => {
      const res = await fetch(`/api/users/${args.id}`, { signal });
      if (!res.ok) throw new Error('Failed to fetch user');
      return res.json();
    },
  },
  key: (args) => args.id,
  expire: 60_000, // cache valid for 60 seconds
});

// Observable usage
userQuery.query({ id: '123' }).subscribe({
  next: (result) => console.log(result.value),
  error: (err) => console.error(err),
});

// Async/await usage
const result = await userQuery.queryAsync({ id: '123' });
console.log(result.value);

Core Concepts

Query

The Query class is the main entry point. It coordinates a QueryClient (handles fetching), a QueryCache (stores results), and a queue operator (controls concurrency). When query() is called, it first checks the cache; if valid data exists, it emits immediately. Otherwise, a new fetch is queued.

QueryClient

The QueryClient manages the execution of fetch functions, including dispatching requests, handling retries on failure, and emitting lifecycle events. It can be shared across multiple Query instances.

QueryCache

The QueryCache stores fetched results keyed by a generated cache key. It supports insertions, mutations, invalidation, trimming, and resets. Multiple Query instances can share a single cache for cross-component data sharing.

QueryTask

A QueryTask represents a single in-flight fetch operation. Tasks are deduplicated by cache key: if two consumers request the same data simultaneously, they share the same task.

Configuration Options

The QueryCtorOptions type defines all configuration for a Query instance:

OptionTypeDescription
clientQueryClient | { fn, options? }The fetch function or an existing QueryClient instance
key(args) => stringGenerates a unique cache key from the query arguments
expirenumberMilliseconds before a cache entry is considered stale
validate(entry, args) => booleanCustom function to validate cache entries
cacheQueryCache | QueryCacheCtorArgsShared cache instance or constructor args
queueOperator'switch' | 'merge' | 'concat' | fnStrategy for handling concurrent requests

Queue Strategies

Queue strategies determine how multiple concurrent requests for different data are handled:

  • switch (default): Cancels the active request when a new one arrives. Best for search-as-you-type where only the latest result matters.
  • merge: Runs all requests in parallel. Best when every request must resolve independently.
  • concat: Queues requests sequentially. Best when execution order matters.
const query = new Query<SearchResult, { term: string }>({
  client: { fn: searchApi },
  key: (args) => args.term,
  queueOperator: 'switch', // cancel stale searches
});

Cache Management

Mutation (Optimistic Update)

Update cached data directly without refetching. The mutate method returns an undo function that restores the previous value:

const undo = userQuery.mutate(
  { id: '123' },
  (current) => ({ value: { ...current!, name: 'Updated Name' }, updated: Date.now() }),
);

// Roll back if the server update fails
try {
  await updateUserOnServer('123', { name: 'Updated Name' });
} catch {
  undo();
}

Invalidation

Mark specific entries or the entire cache as stale, forcing a refetch on the next query:

userQuery.invalidate({ id: '123' }); // invalidate one entry
userQuery.invalidate();                // invalidate all entries

Custom Cache Validation

Override the default expiration-based validation with custom logic:

const query = new Query<Data, Args>({
  client: { fn: fetchData },
  key: (args) => JSON.stringify(args),
  validate: (entry, args) => {
    // Consider invalid if older than 30 minutes
    return (entry.updated ?? 0) + 30 * 60_000 > Date.now();
  },
});

Shared Cache

Multiple Query instances can share a QueryCache to avoid duplicate fetches across components:

import { Query } from '@equinor/fusion-query';
import { QueryCache } from '@equinor/fusion-query/cache';

const sharedCache = new QueryCache();

const homepageQuery = new Query({
  client: { fn: fetchPosts },
  cache: sharedCache,
  key: () => 'posts',
});

const sidebarQuery = new Query({
  client: { fn: fetchPosts },
  cache: sharedCache,
  key: () => 'posts', // same key — reuses cached data
});

Persistent Query

Use persistentQuery when the UI should reflect cache mutations in real time. Unlike query(), which completes after emitting the result, persistentQuery() keeps the subscription open and re-emits whenever the underlying cache entry is updated:

userQuery.persistentQuery({ id: '123' }).subscribe((result) => {
  renderUser(result.value); // re-renders on cache mutation
});

Event System

The Query.event$ Observable aggregates events from three sources — the Query itself, the QueryClient, and the QueryCache — into a single stream for monitoring and debugging.

Query Events

EventDescription
query_createdA new query was initiated
query_completedA query finished (from cache or fetch)
query_connectedSubscriber joined an existing in-flight task
query_queuedA task was added to the processing queue
query_abortedA query was cancelled via AbortSignal
query_cache_hitValid cached data was found
query_cache_missNo valid cache; fetch is required
query_cache_addedFetched data was written to cache
query_job_createdA new fetch job was created
query_job_selectedA job was picked by the queue operator
query_job_startedA job began executing
query_job_closedA job subscription was closed
query_job_completedA job finished and its results were cached
query_job_skippedA job was skipped (no longer observed)

QueryClient Events

EventDescription
query_client_job_requestedA fetch was requested with arguments
query_client_job_executingFetch execution started
query_client_job_completedFetch completed successfully
query_client_job_failedFetch execution failed
query_client_job_canceledFetch was canceled
query_client_job_errorAn irrecoverable error occurred

QueryCache Events

EventDescription
query_cache_entry_setA cache entry was set
query_cache_entry_insertedA new entry was inserted
query_cache_entry_removedAn entry was removed
query_cache_entry_invalidatedAn entry was invalidated
query_cache_entry_mutatedAn entry was mutated
query_cache_trimmedThe cache was trimmed
query_cache_resetThe cache was reset

Subscribing to Events

import { filter } from 'rxjs';
import { QueryEvent } from '@equinor/fusion-query';
import { QueryClientEvent } from '@equinor/fusion-query/client';
import { QueryCacheEvent } from '@equinor/fusion-query/cache';

// All events
query.event$.subscribe((event) => console.log(event.type, event));

// Filter by event type
query.event$.pipe(
  filter((event) => event.type === 'query_cache_hit'),
).subscribe((event) => console.log('Cache hit:', event.data));

// Filter by event source using instanceof
const cacheEvents$ = query.event$.pipe(filter((e) => e instanceof QueryCacheEvent));
const clientEvents$ = query.event$.pipe(filter((e) => e instanceof QueryClientEvent));
const queryEvents$ = query.event$.pipe(filter((e) => e instanceof QueryEvent));

React Integration

The @equinor/fusion-query/react sub-path exports the useDebounceQuery hook for debounced data fetching in React components:

import { useDebounceQuery } from '@equinor/fusion-query/react';

function SearchComponent() {
  const [term, setTerm] = useState('');
  const result = useDebounceQuery(myQuery, {
    args: [{ search: term }],
    delay: 300,
  });

  return <input value={term} onChange={(e) => setTerm(e.target.value)} />;
}

Operators

The operators namespace provides RxJS operator utilities:

  • switchQueue: Cancels in-progress work when new items arrive (default strategy).
  • mergeQueue: Processes items concurrently without cancellation.
  • concatQueue: Processes items sequentially in FIFO order.
  • queryValue: Extracts the raw value from a query result stream, stripping metadata.
import { operators } from '@equinor/fusion-query';

query.query({ id: '123' }).pipe(operators.queryValue).subscribe((user) => {
  console.log(user.name); // plain User object, no wrapper
});

Cleanup

Always unsubscribe from query Observables or call complete() to release resources:

const sub = query.query(args).subscribe((r) => console.log(r));
// Later:
sub.unsubscribe();

// Or dispose the entire Query instance:
query.complete();

Sub-path Exports

PathContents
@equinor/fusion-queryQuery, types, operators, events
@equinor/fusion-query/cacheQueryCache, QueryCacheEvent, cache types
@equinor/fusion-query/clientQueryClient, QueryClientError, QueryClientEvent, client types
@equinor/fusion-query/operatorsQueue operators and queryValue
@equinor/fusion-query/reactuseDebounceQuery hook

版本与发布记录

@equinor/fusion-query 共发布 115 个版本,首次发布于 2022/12/01;过去 12 个月发布了 19 个版本。

版本发布日期距今
7.0.8latest2026/11/121 天前
7.0.8-next.0next2026/11/112 天前
7.0.72026/10/1727 天前
7.0.7-trusted-publishing-20261015-9dfe7602cd2a49a8a9df00d62c26e7304a82a85atrusted-publishing-202610152026/10/1629 天前
7.0.62026/10/121 个月前
7.0.52026/09/012 个月前

其他发布通道:apploader → 5.2.9-apploader-be21a6b60eb13eee8bf869e75002d8634e9c50a9,cli-search-index → 6.0.2-cli-search-index.0,msal-v5 → 6.0.4-msal-v5.0,trusted-publishing-20261015 → 7.0.7-trusted-publishing-20261015-9dfe7602cd2a49a8a9df00d62c26e7304a82a85a,next → 7.0.8-next.0。

关于 @equinor/fusion-query 的常见问题

@equinor/fusion-query 是什么?

Reactive data fetching and caching library with observable streams and comprehensive event system

如何安装 @equinor/fusion-query?

在项目目录运行 npm install @equinor/fusion-query,或使用 pnpm add / yarn add / bun add。

@equinor/fusion-query 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@equinor/fusion-query 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@equinor/fusion-query 可以商用吗?

@equinor/fusion-query 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@equinor/fusion-query 的下载量有多少?

根据 npm 官方下载统计,@equinor/fusion-query 在 2026/10/14 – 2026/11/12 这 30 天内被下载 12,873 次,日均约 429 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@equinor/fusion-query 还在维护吗?

最新版本 7.0.8 发布于 2026/11/12(1 天前);至今共发布 115 个版本,其中过去 12 个月发布了 19 个;首次发布于 2022/12/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。