Skip to content

v6.0.0

Choose a tag to compare

@Ahoo-Wang Ahoo-Wang released this 04 Oct 02:55
9938c62

Highlights

  • Wow packages moved to the Wow repository (#1905). The packages coupled to
    Wow are now developed, tested against the
    Wow server and released with Wow, under typescript/ in the Wow repository.
    Their first stable release was Wow 9.2.0; from Wow 9.2.1 they accept fetcher 6. fetcher is back to being
    a general-purpose HTTP client.
  • @ahoo-wang/fetcher-react is redesigned around cancellation. A request
    is identified by its AbortController, execute never rejects, and queries
    are controlled. Some hooks and options are removed; see
    @ahoo-wang/fetcher-react is redesigned around cancellation under
    Breaking for the table and migration steps.
  • No breaking API change elsewhere. @ahoo-wang/fetcher, decorator,
    eventbus, eventstream, openai, openapi, storage and cosec keep
    their API. Behavior that was a defect no caller can want
    (a query sent as ?a=undefined, a timeout dropped when a signal is passed)
    is corrected; those corrections are listed under Changed, other bug fixes
    under Fixed. The Wow packages accept either fetcher line
    (^5.1.5 || ^6.0.0 from Wow 9.2.1), so you can switch to them first and upgrade fetcher
    afterwards. @ahoo-wang/wow-react does not depend on fetcher-react at all;
    it has its own request state machine.

Breaking

Packages no longer published from fetcher.

5.x package Replacement
@ahoo-wang/fetcher-wow @ahoo-wang/wow-client — the same client under its new name, with the /query/locale/zh_CN and /en_US subpaths
@ahoo-wang/fetcher-generator @ahoo-wang/wow-generator — command wow-generator; fetcher-generator stays as an alias until Wow v10
@ahoo-wang/fetcher-viewer None in 6.x. Deprecated on npm; it stays on the 5.x line. @ahoo-wang/wow-view-engine (on npm from Wow 9.2.0) replaces it with a different model and API
@ahoo-wang/fetcher-view-engine Was never published. It continues as @ahoo-wang/wow-view-engine, on npm from Wow 9.2.0

The last 5.x versions of fetcher-wow and fetcher-generator (5.1.5) stay on
npm, and every version of both is marked deprecated with a pointer to its
replacement (@ahoo-wang/wow-client, @ahoo-wang/wow-generator).
fetcher-viewer depends on fetcher-wow and the 5.x fetcher-react (all peers
^5.0.0), so an application that uses it stays on fetcher 5.x as a whole.

@ahoo-wang/fetcher-react no longer exports the Wow query hooks. They moved
unchanged to @ahoo-wang/wow-react:

  • useSingleQuery, useListQuery, usePagedQuery, useCountQuery,
    useListStreamQuery, and their Use…QueryOptions / Use…QueryReturn types;
  • useFetcherSingleQuery, useFetcherListQuery, useFetcherPagedQuery,
    useFetcherCountQuery, useFetcherListStreamQuery, and their
    UseFetcher…QueryOptions / UseFetcher…QueryReturn types.

@ahoo-wang/wow-react is ESM only; the fetcher-react root entry's UMD build
no longer carries these hooks.

@ahoo-wang/fetcher-react no longer exports the data-monitor hooks. They
were added for the viewer's "data monitor" toggle, the viewer was their only
user, and they stay with it on 5.x. There is no replacement in 6.x: remove
useDataMonitor, UseDataMonitorOptions, UseDataMonitorReturn,
DataMonitorService, dataMonitorService, DataMonitorNotificationConfig,
useDataMonitorEventBus, UseDataMonitorEventBusReturn, DataChangedEvent and
dataMonitorEventBus, or stay on 5.x. (They polled a total every 30 seconds in
each tab; a server-side subscription in Wow's view engine is planned instead.)
The browser-notification helpers they used internally were never exported, so
their removal changes nothing else.

@ahoo-wang/fetcher-react no longer declares @ahoo-wang/fetcher-wow as a
peer.
5.1.3 had made it optional; 6.0 drops it, and neither the modules nor
the type declarations of any entry reference it.

@ahoo-wang/fetcher-react is redesigned around cancellation. A request
is identified by its AbortController. Only the current execution may write
state; a newer execution, abort(), reset() and unmounting all abort it.
State is one value, { status, loading, result, error }. execute never
rejects: it resolves to the state this execution ended in (idle when it was
cancelled). Queries are controlled: the query lives in your state and is
passed as query, and the hook executes when its content (compared deeply)
changes; undefined means not ready. An inline execute option that sets
state no longer re-runs the query on every render: the latest execute is
used, and the query re-runs only when its content changes or autoExecute
turns on.

Removed or changed in 6.0 What to do
useFullscreen, FullscreenProvider, FullscreenContext, useFullscreenContext, the fullscreen utilities (enterFullscreen, exitFullscreen, getFullscreenElement, isFullscreen, …) No replacement; write your own or use a library such as ahooks
useRefs, useForceUpdate, useMounted, useRequestId No replacement; write your own or use a library such as ahooks
useQueryState, isValidateQuery, UseQueryStateOptions, UseQueryStateReturn Pass a controlled query
propagateError option Read the state execute resolves to
initialQuery, setQuery, getQuery on useQuery, useFetcherQuery, the query API hooks and the debounced query hooks Keep the query in your own useState and pass it as query
run, cancel, isPending, setQuery on useDebouncedQuery / useDebouncedFetcherQuery pending, flush(), execute()
onBeforeExecute on execute and query API hooks; the OnBeforeExecuteCallback type Prepare the arguments before calling execute
UseApiMethodExecuteOptions<TArgs, TData, E> UseApiMethodExecuteOptions<TData, E> (first type parameter removed)
useQuery's execute option (query, attributes, abortController) (query, abortController)
usePromiseState's onSuccess / onError options; setters that called them Setters are synchronous and call nothing; use useExecutePromise's callbacks
DepsCapable type Remove the reference; nothing replaces it
API factories turning getter-provided functions into hooks Expose the method as a method or own property; getters are never run
-const { result, setQuery } = useQuery({
-  initialQuery: { keyword: '' },
-  execute: (query, attributes, abortController) => api.search(query, abortController),
-});
+const [query, setQuery] = useState({ keyword: '' });
+const { result } = useQuery({
+  query,
+  execute: (query, abortController) => api.search(query, abortController),
+});
-try {
-  await execute(supplier); // propagateError: true
-  navigate('/done');
-} catch (error) {
-  toast(error.message);
-}
+const { status, error } = await execute(supplier);
+if (status === 'success') navigate('/done');
+else if (status === 'error') toast(error.message);
-const { setQuery, run } = useDebouncedQuery({ initialQuery, execute, debounce: { delay: 300 } });
+const [query, setQuery] = useState(initialQuery);
+const { result, pending, flush } = useDebouncedQuery({ query, execute, debounce: { delay: 300 } });

These still compile but behave differently:

  • reset() cancels the execution in flight first. It used to only set idle,
    and the request could write its result back later.
  • onAbort is called synchronously and no longer delays the next execution.
  • useFetcher no longer writes request.abortController; the caller's
    request object is not modified. Cancel through abort()/reset(), or pass
    request.signal, which still applies.
  • On failure, useFetcher's exchange is the failed request's exchange (from
    the ExchangeError, so exchange.response.status of a 404 is readable), not
    undefined.
  • A query that executes on mount renders loading on its first render (it
    rendered idle), unless initialStatus is given.
  • useEventSubscription subscribes once per bus and handler name, order
    and once, and calls the latest handle; it no longer resubscribes when
    only the handler object's identity changes.
  • With the old propagateError: true, automatic and debounced executions
    produced unhandled promise rejections; execute no longer rejects.

Upgrade check: run tsc; then search for propagateError, reset(,
request.abortController, and tests that assert idle on the first render of
an auto-executing query.

The UMD bundle moved from dist/index.umd.js to dist/index.umd.cjs in
@ahoo-wang/fetcher-cosec, fetcher-eventbus, fetcher-openai,
fetcher-openapi, fetcher-storage and fetcher-react. Inside a
"type": "module" package a .js file is an ES module to Node, so require()
of these packages did not load the UMD bundle; .cjs fixes that and matches
fetcher, fetcher-decorator and fetcher-eventstream. Installing and
importing by package name is unaffected. If you load the bundle by URL from a
CDN, update the path, for example
https://unpkg.com/@ahoo-wang/fetcher-cosec@6/dist/index.umd.cjs. (The 5.x
line keeps publishing both paths.)

Changed

  • Peer ranges follow the release. Every package declares its sibling
    peers as workspace:^, which publishing turns into ^6.0.0; they were
    pinned to ^5.0.0. @ahoo-wang/fetcher-react no longer declares
    @ahoo-wang/fetcher-eventstream or react-dom as peers (nothing imports
    them), and accepts react ^19.0.0 (was ^19.3.0, the repository's own
    development version). dequal is no longer bundled into its ES module build;
    it is installed as the dependency it was already declared as. Engineering
    Quality checks these (.github/scripts/package-contract.mjs).

  • @ahoo-wang/fetcher: query parameters are serialized one way, by
    UrlBuilder. undefined and null values are omitted (they were sent as
    the text undefined/null), an array becomes one parameter per item
    (ids=1&ids=2, was ids=1%2C2), and a Date becomes its ISO 8601 form.
    A URLSearchParams is still taken as is.

  • @ahoo-wang/fetcher: a path placeholder without a value throws
    Missing required path parameter also when the request has no path
    parameters at all, or the value is null; the URL used to be sent with
    {id} still in it. Express placeholders end at the first non-identifier
    character: /files/:name.json is the parameter name, not name.json.

  • @ahoo-wang/fetcher: no Content-Type by default. It follows the body:
    application/json for a plain object (serialized) and for a string, the
    type fetch derives for Blob, FormData and URLSearchParams, none for
    binary bodies. Every Fetcher used to send Content-Type: application/json
    on every request, so each cross-origin GET needed a CORS preflight and
    binary bodies were mislabeled. A Content-Type you set yourself, on the
    fetcher or the request, is still sent.

  • @ahoo-wang/fetcher: a request rejected by the status check rejects with
    the HttpStatusValidationError itself (an ExchangeError, with the
    exchange), so error instanceof HttpStatusValidationError holds. It used to
    be wrapped in a plain ExchangeError; error.exchange.error still returns
    it, but error.cause no longer does. Other errors (a timeout, a network
    failure, an interceptor's own error) are still wrapped, with the original as
    cause.

  • @ahoo-wang/fetcher: a timeout applies together with the caller's signal
    and abortController; whichever fires first aborts the request. Passing a
    signal used to switch the timeout off. A timeout no longer aborts the
    caller's AbortController and nothing is written to the request object.

  • @ahoo-wang/fetcher: an error interceptor that throws, or a callback it
    runs (such as @ahoo-wang/fetcher-cosec's onUnauthorized or
    onForbidden), no longer escapes the exchange as a raw error. The throw
    becomes exchange.error and the exchange rejects with an ExchangeError
    whose cause is the thrown value, like any other failure; later error
    interceptors do not run. In @ahoo-wang/fetcher-cosec, when a refresh fails
    because such a callback threw on the refresh request, RefreshTokenError's
    cause is now that request's ExchangeError, whose cause is the callback
    error.

  • @ahoo-wang/fetcher: FetcherError keeps its own stack, where the failure
    surfaced; it used to copy the stack of its cause. The original failure and
    its stack stay on cause.

  • @ahoo-wang/fetcher-react/fetcher (added in 5.1.3) and /core load no
    security, storage or event-bus integration; the build verifies this.

  • The agent skills plugin (skills/) drops fetcher-wow-cqrs,
    fetcher-openapi-generator and fetcher-viewer-components; the first two
    continue in the Wow repository's skills. fetcher-react-hooks no longer
    covers the Wow or data-monitor hooks.

  • Storybook no longer carries the Wow query, viewer and view-engine stories;
    the Wow ones are in the Wow Storybook.

  • @ahoo-wang/fetcher-decorator: arguments are bound by shape, and fetcher
    serializes them. A plain object passed to @path, @query or @header is
    still spread into its keys; an array, a Date or any other value is bound to
    the parameter's name (arrays and dates were spread into 0=…&1=… or lost).
    So @query('ids') ids: number[] sends ids=1&ids=2, and an array header is
    sent as a comma-separated list. undefined/null entries of a spread object
    are left out. An explicitly named @attribute('user') stores its value under
    user even when it is an object (it was spread); an unnamed @attribute()
    still merges a record or Map.

  • @ahoo-wang/fetcher-decorator: a subclass that overrides an inherited
    endpoint method without decorating the override keeps its own
    implementation; @api used to replace it with the parent's request.

  • @ahoo-wang/fetcher-openapi: types follow the specification for 3.0 and
    3.1 documents. Info.title, Info.version and Response.description are
    required; SecurityScheme.in excludes path; SecurityRequirement is not
    extensible. Added from 3.1: Info.summary, License.identifier,
    OpenAPI.webhooks, OpenAPI.jsonSchemaDialect, Components.pathItems,
    the mutualTLS security scheme type, and on Schema the optional JSON
    Schema 2020-12 keywords 3.1 uses ($id, $anchor, $defs, examples,
    contentMediaType, prefixItems, contains, unevaluatedItems,
    patternProperties, propertyNames, dependentRequired,
    dependentSchemas, unevaluatedProperties, if/then/else, …).

  • @ahoo-wang/fetcher-eventstream: the end of a stream is reported
    truthfully. A final line cut off before its line terminator (a lost
    connection) is dropped instead of dispatched as data; an event whose lines
    all arrived but whose closing blank line did not is still dispatched. With a
    terminate detector, a stream that ends without the terminating event errors
    with the new EventStreamIncompleteError instead of ending normally — for
    @ahoo-wang/fetcher-openai, a completion stream that ends before
    data: [DONE]. retry is reported only on the event whose block set it.

  • @ahoo-wang/fetcher-storage: a stored value that cannot be deserialized is
    removed with a warning and read as absent (the default value). It used to
    throw from get(), and so from set() and remove() too: one corrupt
    cosec-token made every request, sign-in and sign-out fail until the user
    cleared site data. set(undefined) removes the value; it stored the text
    undefined, which then failed to parse.

  • @ahoo-wang/fetcher-cosec: a new isTrusted option (on CoSecConfig,
    CoSecRequestOptions and AuthorizationInterceptorOptions) decides which
    absolute request URLs carry the token and the CoSec headers. By default
    every request carries them, as before; pass the exported sameOriginTrust
    to keep them to the origin of the fetcher's baseURL and the page's own
    origin, so a pagination link, a download URL or a callback on another
    origin never receives the access token or the device ID. A relative request
    URL is always trusted.

  • @ahoo-wang/fetcher-cosec: a JWT whose payload is not a JSON object (123,
    "x", an array) is invalid: parseJwtPayload returns null and the token
    reads as expired. It used to read as a token that never expires.

  • @ahoo-wang/fetcher-storage and @ahoo-wang/fetcher-cosec: destroy()
    also closes the event bus the storage created for itself (KeyStorage's
    default bus, the broadcast bus of TokenStorage, DeviceIdStorage and
    SpaceIdStorage); a bus passed in eventBus is left open. Creating and
    destroying storages no longer leaks a BroadcastChannel each time.

  • @ahoo-wang/fetcher-react: RouteGuard calls onUnauthorized in an
    effect after the render commits, once each time the user becomes (or starts
    out) unauthenticated. It was called during render, on every render (twice
    under StrictMode), where a navigate() triggers React warnings.

  • @ahoo-wang/fetcher-react: useKeyStorage (and so useSecurity and
    SecurityProvider) renders the default value on the server and during
    hydration, then the stored value. It read browser storage during hydration,
    which does not match the server HTML (a hydration error in Next.js and
    similar). Client-only rendering is unchanged.

  • @ahoo-wang/fetcher-react: useLatest updates its ref after the render
    commits (in an insertion effect), not during render, so a render React
    discards never leaves its values in the ref.

  • @ahoo-wang/fetcher-react: execute-API hooks (createExecuteApiHooks)
    accept appendAbortController: true, which calls the method as
    method(...params, abortController), so replacing or unmounting an
    execution cancels a @api method's request instead of only discarding its
    result. Off by default; existing calls are unchanged.

  • @ahoo-wang/fetcher-react: useSecurity (and SecurityProvider)
    re-renders when the refresh token expires, so authenticated turns false
    and RouteGuard reacts on an idle page. It used to stay true until
    something else re-rendered. An expired access token that can still be
    refreshed does not re-render: the next request renews it.

  • @ahoo-wang/fetcher-cosec: in a browser with Web Locks, tabs sharing a
    token storage on localStorage refresh one at a time (lock
    cosec-refresh:<key>). A tab that waited re-reads storage and reuses the
    token another tab just stored for the same session, so a one-time refresh
    token no longer signs the user out when two tabs refresh together. A refresh
    that never settles now also holds the other tabs' refreshes until it ends;
    set a timeout on the refresh client. Outside a browser (one server process
    serves many users under the same key) and without Web Locks, tabs refresh
    independently as before.

  • @ahoo-wang/fetcher-cosec: a custom isTrusted is asked once per request,
    and the 401 retry reuses that decision. It used to be asked again for the
    retry, after the URL had become absolute, so a predicate listing only extra
    origins dropped Authorization from the retried request.

  • @ahoo-wang/fetcher-cosec: a refresh response without string accessToken
    and refreshToken is a failed refresh (RefreshTokenError, token removed).
    It used to be stored and sent as Bearer undefined.

  • @ahoo-wang/fetcher-decorator: executors are no longer cached per
    instance. An overriding endpoint and the parent endpoint it calls through
    super.method() each send their own request (whichever ran first used to
    answer for both), and an instance's apiMetadata applies on the next call
    whether it is replaced or changed in place.

  • @ahoo-wang/fetcher-decorator: the unbound path placeholder warning fires
    once per endpoint, and only when an unnamed @path() parameter's inferred
    name matches no placeholder (the sign a minifier renamed it). It used to
    warn on every request about placeholders an interceptor fills, such as the
    {tenantId}/{ownerId} of cosec's ResourceAttributionRequestInterceptor.

  • @ahoo-wang/fetcher-eventbus: EventBus.emit creates the type's bus on
    first use, so a tab that only emits through a BroadcastTypedEventBus
    supplier reaches other tabs. After destroy(), on/emit start a fresh
    bus. BroadcastTypedEventBus.emit after destroy() runs the local handlers
    and no longer posts to the closed channel (it rejected with
    InvalidStateError); a post failure on an open bus still rejects.

  • @ahoo-wang/fetcher-eventbus no longer has a peer dependency on
    @ahoo-wang/fetcher; EventHandler declares name and order itself,
    with the same shape.

  • @ahoo-wang/fetcher-storage: KeyStorage updates its cache before any
    other listener runs, so a slow listener can no longer briefly roll get()
    back to an older value. addListener returns a no-op remover when the name
    is already taken, and a remover never removes a different listener with the
    same name.

  • @ahoo-wang/fetcher-eventstream: converting a response whose body was
    already read or locked throws EventStreamConvertError ('Response body is already used'), as documented, instead of a bare TypeError.

  • @ahoo-wang/fetcher-cosec: only the server rejecting the refresh token
    signs the user out. When a refresh fails and storage still holds the same
    token, the token is removed and RefreshTokenError is thrown (so
    onUnauthorized fires) only if the refresh endpoint answered with a 4xx
    (error.exchange.response.status, as the fetcher's ExchangeError carries
    it) or with a body that is not a composite token. A network error, timeout,
    abort or 5xx keeps the session and throws the new RefreshUnavailableError,
    which UnauthorizedErrorInterceptor does not report; a later request
    refreshes again. Every refresh failure used to sign the user out. A custom
    TokenRefresher signals a rejection the same way: throw an error whose
    exchange.response.status is the 4xx.

  • @ahoo-wang/fetcher-eventbus: BroadcastTypedEventBus.destroy() closes only
    a messenger it created. A messenger passed in options.messenger belongs to
    the caller: the bus detaches from it and leaves it open. It used to close it.

Added

  • @ahoo-wang/fetcher: a fetch option (FetcherOptions.fetch, type
    FetchImplementation) sets the fetch requests are sent with, for a
    runtime or framework that supplies its own (Tauri, instrumentation) or a
    test. It defaults to the global fetch, read at call time; timeouts and
    signals still apply. Like validateStatus, it configures the default
    interceptors, so it has no effect when you pass your own interceptors
    manager; build that with new InterceptorManager(validateStatus, fetch).

  • @ahoo-wang/fetcher-eventstream: jsonEventStreamResultExtractor(detector?)
    builds a JSON event stream extractor that ends at the event detector
    recognizes (not parsed or yielded) and errors with
    EventStreamIncompleteError when the stream ends before it.
    @ahoo-wang/fetcher-openai's CompletionStreamResultExtractor is now
    jsonEventStreamResultExtractor(DoneDetector).

  • @ahoo-wang/fetcher-openai: OpenAIOptions accepts the other
    FetcherOptions (timeout, fetch, headers, interceptors, …) for the
    client's Fetcher; apiKey still sets Authorization, over a header of the
    same name.

Fixed

  • Error subclasses pass instanceof for themselves in every package:
    EventStreamConvertError and EventStreamIncompleteError
    (@ahoo-wang/fetcher-eventstream), RefreshTokenError and
    RefreshSessionChangedError (@ahoo-wang/fetcher-cosec) no longer reset
    their prototype, which made a subclass's objects instances of the parent
    only.

  • @ahoo-wang/fetcher-decorator: endpoints declared on symbol-named methods
    are bound; they kept the placeholder body and threw autoGeneratedError.

  • Package homepage links point to the main branch, and
    @ahoo-wang/fetcher-decorator's repository directory is packages/decorator.

  • @ahoo-wang/fetcher-react: useEventSubscription removes only a
    subscription it made: when the handler name was already taken, unmounting
    no longer removes the other component's handler. useFetcher no longer
    keeps the previous request's exchange after a failure; it exposes the
    failed request's exchange.

  • @ahoo-wang/fetcher-cosec: when a refresh fails because another tab has
    already used the one-time refresh token and stored the new token, the tab
    re-reads storage (new KeyStorage.reload()) and continues with that token.
    It used to remove the other tab's fresh token and sign both tabs out.

  • @ahoo-wang/fetcher-decorator: parameter names are inferred correctly for
    methods with destructured parameters or defaults that contain commas
    (({ a, b }, c), (p = [1, 2], q)); a destructured parameter has no
    inferred name. The executor cache no longer adds a requestExecutors field
    to service instances, and replacing an instance's apiMetadata takes effect
    on the next call. Calling a method detached from its instance, or compiling
    with standard (TC39) decorators, fails with an error that says so. The
    unbound-placeholder warning uses the fetcher's template style and the path
    parameters from every layer, @request included.

  • @ahoo-wang/fetcher-eventstream: a long line split across many chunks is
    processed in linear time (the buffer was re-split on every chunk: 4 MB took
    over a second). The ReadableStream async-iterator polyfill (older Safari)
    cancels the stream and rethrows on throw(), reports done after it is
    released, and its docs no longer show releasing the lock before break,
    which left the connection open.

  • @ahoo-wang/fetcher-openai: chat.completions(request, signal?) accepts an
    AbortSignal; ChatResponse.usage is optional (streamed chunks carry
    none), ChatRequest.stream_options is typed, and the JSDoc examples use
    options that exist.

  • @ahoo-wang/fetcher-storage: InMemoryStorage.setItem stores
    String(value), as Web Storage does.

  • @ahoo-wang/fetcher-eventbus: a BroadcastChannelMessenger no longer keeps
    a Node process alive (the channel is unrefed), so a script or server that
    creates a cross-tab bus can exit. StorageMessenger.close() removes the
    messages it wrote instead of leaving them in localStorage.

  • @ahoo-wang/fetcher-storage: the UMD bundle defines window.FetcherStorage.
    It defined window.Fetcher, overwriting the core package's global, and the
    UMD bundles of fetcher-cosec and fetcher-react, which read
    FetcherStorage, got undefined.

  • @ahoo-wang/fetcher: every Fetcher owns its default headers; all
    instances created without headers shared one object, so a header set on
    one appeared on all of them.

  • @ahoo-wang/fetcher: interceptors work on a copy of the request's
    urlParams. A path or query parameter an interceptor added (for example
    the tenant from @ahoo-wang/fetcher-cosec's
    ResourceAttributionRequestInterceptor) no longer stays on a request object
    the caller reuses.

  • @ahoo-wang/fetcher: a ReadableStream body is sent with
    duplex: 'half', which fetch requires; such requests used to fail.

  • @ahoo-wang/fetcher: the fetcher registrar lives on globalThis, so when a
    process loads the package twice (its ES module and CommonJS builds, or two
    versions) both copies see the same named fetchers and the same default
    fetcher. getFetcher accepts a fetcher created by either copy.

  • @ahoo-wang/fetcher: a subclass of FetcherError, ExchangeError,
    HttpStatusValidationError or FetchTimeoutError passes instanceof
    without calling Object.setPrototypeOf itself. The base classes reset the
    prototype to their own, which broke subclasses that did not reset it again.

  • @ahoo-wang/fetcher: fetch receives a clean RequestInit. The
    fetcher-only fields url, timeout, urlParams and abortController were
    passed along in it; the URL is the first argument and cancellation is the
    merged signal.

  • Type declarations resolve under moduleResolution: node16/nodenext: every
    package ships .d.cts for its require condition next to .d.ts for
    import, and relative specifiers carry their extension (#1916). CommonJS
    consumers no longer get TS1479/TS2834 errors, or any under skipLibCheck.

  • @ahoo-wang/fetcher-cosec: the 401 refresh-retry in
    AuthorizationResponseInterceptor replays only the request phase and the
    response interceptors up to itself, instead of the whole exchange (#1249).
    A failed retry no longer runs the error interceptors twice (for example
    onForbidden firing twice when the retry answers 403) and the rejected ExchangeError now
    carries the retry's own HttpStatusValidationError in exchange.error
    rather than a nested ExchangeError; a successful retry no longer runs the response
    interceptors ordered after it twice on the fresh response.

  • @ahoo-wang/fetcher-eventstream: the global Response augmentation declares
    contentType and isEventStream as readonly properties (they were
    getters). Getters do not merge across declarations, so a project whose
    compilation sees both the ESM (.d.ts) and CommonJS (.d.cts) declarations
    (both .mts and .cts consumers) failed with TS2300: Duplicate identifier
    (#1920; also in 5.1.5).

Maintenance

  • 5.x line. The 5.x branch (cut from v5.1.3) keeps receiving fixes.
    After 6.0, 5.x patches are published under the npm dist-tag release-5, so
    latest stays on 6.x; install one with @ahoo-wang/fetcher@release-5.
  • Dependency direction. Engineering Quality fails if any workspace
    manifest, pnpm.overrides, catalog entry or package source depends on
    @ahoo-wang/wow-*. Wow depends on fetcher, never the other way round.
  • Downstream Wow check. downstream-wow.yml builds and packs the core
    packages when a change touches one that Wow consumes (fetcher,
    decorator, eventstream, openapi), links
    them into a checkout of Wow through pnpm.overrides, and runs Wow's
    TypeScript build, type check and unit tests. It is advisory, not required.
  • CI runs a single core suite on Node 22 and 24; the view-engine shards, the
    viewer suite and generator-test.yml (the Wow server version matrix) left
    with their packages. Release admission requires CI, Engineering Quality,
    Build Storybook and Integration Test, and Codecov; it no longer requires
    Codacy, which stopped analysing the repository unnoticed and first held this
    release (5.x dropped it in #1919).
  • Integration tests cover the core packages only; the Wow client and
    generated-code cases run in the Wow repository. The required integration run
    uses no external network: the JSONPlaceholder suites run against a local
    json-server with a fixture of the live data (#1926). The cases that reach
    external hosts (the OpenAI-compatible endpoint, the live JSONPlaceholder
    site) run in the advisory integration-external.yml, outside release
    admission (#1924).
  • Renovate widens peer ranges for a new major instead of raising their
    floor (renovate.json): a peer range states the oldest version a package
    supports, not the one it is developed with.
  • Package types. pnpm check:package-types packs every published package,
    checks it with @arethetypeswrong/cli, and type-checks an .mts and a
    .cts consumer of it in one tsc run under node16 and nodenext, which
    catches declaration clashes the per-entry check cannot (#1916, #1920).
  • Agent skills. The skills/ plugin was rewritten for triggering and
    accuracy, gained fetcher-v6-migration (5.x → 6.0, including when to stay on
    5.x), and is guarded in CI (every imported name must be a real export). Each
    skill has claude plugin eval cases; pnpm eval:skills runs them (not in CI)
    (#1913, #1915).

Upgrading from 5.x

If you use none of the packages above and not @ahoo-wang/fetcher-react, bump
every @ahoo-wang/fetcher* dependency to ^6.0.0; nothing else changes. If
you use @ahoo-wang/fetcher-react but none of the moved packages, bump to
^6.0.0 and follow @ahoo-wang/fetcher-react is redesigned around
cancellation
under Breaking.

Otherwise, switch to the Wow packages first — they work with the latest
fetcher 5.x —
then upgrade fetcher:

# 1. On 5.x, move to the latest 5.x (fetcher-react's fetcher-wow peer is optional from 5.1.3)
pnpm add @ahoo-wang/fetcher@^5.1.5 @ahoo-wang/fetcher-react@^5.1.5 # and the others you use
# 2. Replace the moved packages
pnpm remove @ahoo-wang/fetcher-wow @ahoo-wang/fetcher-generator
pnpm add @ahoo-wang/wow-client@9.2.1 @ahoo-wang/wow-react@9.2.1
pnpm add -D @ahoo-wang/wow-generator@9.2.1
# 3. Upgrade fetcher
pnpm add @ahoo-wang/fetcher@^6 @ahoo-wang/fetcher-react@^6 # and the others you use
-import { SnapshotQueryClient } from '@ahoo-wang/fetcher-wow';
+import { SnapshotQueryClient } from '@ahoo-wang/wow-client';

-import { zh_CN } from '@ahoo-wang/fetcher-wow/query/locale/zh_CN';
+import { zh_CN } from '@ahoo-wang/wow-client/query/locale/zh_CN';

-import { useFetcher, usePagedQuery } from '@ahoo-wang/fetcher-react';
+import { useFetcher } from '@ahoo-wang/fetcher-react';
+import { usePagedQuery } from '@ahoo-wang/wow-react';
 "scripts": {
-  "generate": "fetcher-generator generate -i http://localhost:8080/v3/api-docs"
+  "generate": "wow-generator generate -i http://localhost:8080/v3/api-docs"
 }

If you use @ahoo-wang/fetcher-react, follow
@ahoo-wang/fetcher-react is redesigned around cancellation under
Breaking after step 3: run tsc, apply the table and the migration diffs,
then check the behavior changes listed there.

fetcher-generator keeps working as an alias of wow-generator until Wow v10.
Code it already generated imports from @ahoo-wang/fetcher-wow; regenerate it (wow-generator emits @ahoo-wang/wow-client),
or replace that import with @ahoo-wang/wow-client.

If you use the data-monitor hooks, stay on 5.x. If you use the deprecated
@ahoo-wang/fetcher-viewer, stay on 5.x until you move to
@ahoo-wang/wow-view-engine, which uses a different model and API.

Published packages

Version 6.0.0 applies to @ahoo-wang/fetcher and its cosec, decorator,
eventbus, eventstream, openai, openapi, react and storage packages.

No longer published from this repository: fetcher-wow,
fetcher-generator and fetcher-viewer (see Breaking).

Release verification

Published on 2026-10-04 from commit 9938c62eed81ccab9907407059d74d37cf40040a on main. All
four required workflows completed successfully against this exact commit.