Repository navigation
v6.0.0
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, undertypescript/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-reactis redesigned around cancellation. A request
is identified by itsAbortController,executenever rejects, and queries
are controlled. Some hooks and options are removed; see
@ahoo-wang/fetcher-reactis redesigned around cancellation under
Breaking for the table and migration steps.- No breaking API change elsewhere.
@ahoo-wang/fetcher,decorator,
eventbus,eventstream,openai,openapi,storageandcoseckeep
their API. Behavior that was a defect no caller can want
(a query sent as?a=undefined, a timeout dropped when asignalis 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.0from Wow 9.2.1), so you can switch to them first and upgrade fetcher
afterwards.@ahoo-wang/wow-reactdoes not depend onfetcher-reactat 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 theirUse…QueryOptions/Use…QueryReturntypes;useFetcherSingleQuery,useFetcherListQuery,useFetcherPagedQuery,
useFetcherCountQuery,useFetcherListStreamQuery, and their
UseFetcher…QueryOptions/UseFetcher…QueryReturntypes.
@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 setidle,
and the request could write its result back later.onAbortis called synchronously and no longer delays the next execution.useFetcherno longer writesrequest.abortController; the caller's
request object is not modified. Cancel throughabort()/reset(), or pass
request.signal, which still applies.- On failure,
useFetcher'sexchangeis the failed request's exchange (from
theExchangeError, soexchange.response.statusof a 404 is readable), not
undefined. - A query that executes on mount renders
loadingon its first render (it
renderedidle), unlessinitialStatusis given. useEventSubscriptionsubscribes once perbusand handlername,order
andonce, and calls the latesthandle; 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;executeno 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 asworkspace:^, which publishing turns into^6.0.0; they were
pinned to^5.0.0.@ahoo-wang/fetcher-reactno longer declares
@ahoo-wang/fetcher-eventstreamorreact-domas peers (nothing imports
them), and acceptsreact^19.0.0(was^19.3.0, the repository's own
development version).dequalis 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.undefinedandnullvalues are omitted (they were sent as
the textundefined/null), an array becomes one parameter per item
(ids=1&ids=2, wasids=1%2C2), and aDatebecomes its ISO 8601 form.
AURLSearchParamsis still taken as is. -
@ahoo-wang/fetcher: a path placeholder without a value throws
Missing required path parameteralso when the request has no path
parameters at all, or the value isnull; the URL used to be sent with
{id}still in it. Express placeholders end at the first non-identifier
character:/files/:name.jsonis the parametername, notname.json. -
@ahoo-wang/fetcher: noContent-Typeby default. It follows the body:
application/jsonfor a plain object (serialized) and for a string, the
type fetch derives forBlob,FormDataandURLSearchParams, none for
binary bodies. EveryFetcherused to sendContent-Type: application/json
on every request, so each cross-originGETneeded a CORS preflight and
binary bodies were mislabeled. AContent-Typeyou set yourself, on the
fetcher or the request, is still sent. -
@ahoo-wang/fetcher: a request rejected by the status check rejects with
theHttpStatusValidationErroritself (anExchangeError, with the
exchange), soerror instanceof HttpStatusValidationErrorholds. It used to
be wrapped in a plainExchangeError;error.exchange.errorstill returns
it, buterror.causeno 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'ssignal
andabortController; whichever fires first aborts the request. Passing a
signalused to switch the timeout off. A timeout no longer aborts the
caller'sAbortControllerand 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'sonUnauthorizedor
onForbidden), no longer escapes the exchange as a raw error. The throw
becomesexchange.errorand the exchange rejects with anExchangeError
whosecauseis 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
causeis now that request'sExchangeError, whosecauseis the callback
error. -
@ahoo-wang/fetcher:FetcherErrorkeeps its own stack, where the failure
surfaced; it used to copy the stack of itscause. The original failure and
its stack stay oncause. -
@ahoo-wang/fetcher-react/fetcher(added in 5.1.3) and/coreload no
security, storage or event-bus integration; the build verifies this. -
The agent skills plugin (
skills/) dropsfetcher-wow-cqrs,
fetcher-openapi-generatorandfetcher-viewer-components; the first two
continue in the Wow repository's skills.fetcher-react-hooksno 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,@queryor@headeris
still spread into its keys; an array, aDateor any other value is bound to
the parameter's name (arrays and dates were spread into0=…&1=…or lost).
So@query('ids') ids: number[]sendsids=1&ids=2, and an array header is
sent as a comma-separated list.undefined/nullentries of a spread object
are left out. An explicitly named@attribute('user')stores its value under
usereven when it is an object (it was spread); an unnamed@attribute()
still merges a record orMap. -
@ahoo-wang/fetcher-decorator: a subclass that overrides an inherited
endpoint method without decorating the override keeps its own
implementation;@apiused 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.versionandResponse.descriptionare
required;SecurityScheme.inexcludespath;SecurityRequirementis not
extensible. Added from 3.1:Info.summary,License.identifier,
OpenAPI.webhooks,OpenAPI.jsonSchemaDialect,Components.pathItems,
themutualTLSsecurity scheme type, and onSchemathe 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 newEventStreamIncompleteErrorinstead of ending normally — for
@ahoo-wang/fetcher-openai, a completion stream that ends before
data: [DONE].retryis 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 fromget(), and so fromset()andremove()too: one corrupt
cosec-tokenmade 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 newisTrustedoption (onCoSecConfig,
CoSecRequestOptionsandAuthorizationInterceptorOptions) decides which
absolute request URLs carry the token and the CoSec headers. By default
every request carries them, as before; pass the exportedsameOriginTrust
to keep them to the origin of the fetcher'sbaseURLand 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:parseJwtPayloadreturnsnulland the token
reads as expired. It used to read as a token that never expires. -
@ahoo-wang/fetcher-storageand@ahoo-wang/fetcher-cosec:destroy()
also closes the event bus the storage created for itself (KeyStorage's
default bus, the broadcast bus ofTokenStorage,DeviceIdStorageand
SpaceIdStorage); a bus passed ineventBusis left open. Creating and
destroying storages no longer leaks aBroadcastChanneleach time. -
@ahoo-wang/fetcher-react:RouteGuardcallsonUnauthorizedin 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 anavigate()triggers React warnings. -
@ahoo-wang/fetcher-react:useKeyStorage(and souseSecurityand
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:useLatestupdates 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)
acceptappendAbortController: true, which calls the method as
method(...params, abortController), so replacing or unmounting an
execution cancels a@apimethod's request instead of only discarding its
result. Off by default; existing calls are unchanged. -
@ahoo-wang/fetcher-react:useSecurity(andSecurityProvider)
re-renders when the refresh token expires, soauthenticatedturnsfalse
andRouteGuardreacts on an idle page. It used to staytrueuntil
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 onlocalStoragerefresh 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 atimeouton 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 customisTrustedis 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 droppedAuthorizationfrom the retried request. -
@ahoo-wang/fetcher-cosec: a refresh response without stringaccessToken
andrefreshTokenis a failed refresh (RefreshTokenError, token removed).
It used to be stored and sent asBearer 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'sapiMetadataapplies 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'sResourceAttributionRequestInterceptor. -
@ahoo-wang/fetcher-eventbus:EventBus.emitcreates the type's bus on
first use, so a tab that only emits through aBroadcastTypedEventBus
supplier reaches other tabs. Afterdestroy(),on/emitstart a fresh
bus.BroadcastTypedEventBus.emitafterdestroy()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-eventbusno longer has a peer dependency on
@ahoo-wang/fetcher;EventHandlerdeclaresnameandorderitself,
with the same shape. -
@ahoo-wang/fetcher-storage:KeyStorageupdates its cache before any
other listener runs, so a slow listener can no longer briefly rollget()
back to an older value.addListenerreturns 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 throwsEventStreamConvertError('Response body is already used'), as documented, instead of a bareTypeError. -
@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 andRefreshTokenErroris thrown (so
onUnauthorizedfires) only if the refresh endpoint answered with a 4xx
(error.exchange.response.status, as the fetcher'sExchangeErrorcarries
it) or with a body that is not a composite token. A network error, timeout,
abort or 5xx keeps the session and throws the newRefreshUnavailableError,
whichUnauthorizedErrorInterceptordoes not report; a later request
refreshes again. Every refresh failure used to sign the user out. A custom
TokenRefreshersignals a rejection the same way: throw an error whose
exchange.response.statusis the 4xx. -
@ahoo-wang/fetcher-eventbus:BroadcastTypedEventBus.destroy()closes only
a messenger it created. A messenger passed inoptions.messengerbelongs to
the caller: the bus detaches from it and leaves it open. It used to close it.
Added
-
@ahoo-wang/fetcher: afetchoption (FetcherOptions.fetch, type
FetchImplementation) sets thefetchrequests are sent with, for a
runtime or framework that supplies its own (Tauri, instrumentation) or a
test. It defaults to the globalfetch, read at call time; timeouts and
signals still apply. LikevalidateStatus, it configures the default
interceptors, so it has no effect when you pass your owninterceptors
manager; build that withnew InterceptorManager(validateStatus, fetch). -
@ahoo-wang/fetcher-eventstream:jsonEventStreamResultExtractor(detector?)
builds a JSON event stream extractor that ends at the eventdetector
recognizes (not parsed or yielded) and errors with
EventStreamIncompleteErrorwhen the stream ends before it.
@ahoo-wang/fetcher-openai'sCompletionStreamResultExtractoris now
jsonEventStreamResultExtractor(DoneDetector). -
@ahoo-wang/fetcher-openai:OpenAIOptionsaccepts the other
FetcherOptions(timeout,fetch,headers,interceptors, …) for the
client'sFetcher;apiKeystill setsAuthorization, over a header of the
same name.
Fixed
-
Error subclasses pass
instanceoffor themselves in every package:
EventStreamConvertErrorandEventStreamIncompleteError
(@ahoo-wang/fetcher-eventstream),RefreshTokenErrorand
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 threwautoGeneratedError. -
Package
homepagelinks point to themainbranch, and
@ahoo-wang/fetcher-decorator's repository directory ispackages/decorator. -
@ahoo-wang/fetcher-react:useEventSubscriptionremoves only a
subscription it made: when the handler name was already taken, unmounting
no longer removes the other component's handler.useFetcherno longer
keeps the previous request'sexchangeafter 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 (newKeyStorage.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 arequestExecutorsfield
to service instances, and replacing an instance'sapiMetadatatakes 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,@requestincluded. -
@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). TheReadableStreamasync-iterator polyfill (older Safari)
cancels the stream and rethrows onthrow(), reportsdoneafter it is
released, and its docs no longer show releasing the lock beforebreak,
which left the connection open. -
@ahoo-wang/fetcher-openai:chat.completions(request, signal?)accepts an
AbortSignal;ChatResponse.usageis optional (streamed chunks carry
none),ChatRequest.stream_optionsis typed, and the JSDoc examples use
options that exist. -
@ahoo-wang/fetcher-storage:InMemoryStorage.setItemstores
String(value), as Web Storage does. -
@ahoo-wang/fetcher-eventbus: aBroadcastChannelMessengerno longer keeps
a Node process alive (the channel isunrefed), so a script or server that
creates a cross-tab bus can exit.StorageMessenger.close()removes the
messages it wrote instead of leaving them inlocalStorage. -
@ahoo-wang/fetcher-storage: the UMD bundle defineswindow.FetcherStorage.
It definedwindow.Fetcher, overwriting the core package's global, and the
UMD bundles offetcher-cosecandfetcher-react, which read
FetcherStorage, gotundefined. -
@ahoo-wang/fetcher: everyFetcherowns its default headers; all
instances created withoutheadersshared 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: aReadableStreambody is sent with
duplex: 'half', which fetch requires; such requests used to fail. -
@ahoo-wang/fetcher: the fetcher registrar lives onglobalThis, 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.getFetcheraccepts a fetcher created by either copy. -
@ahoo-wang/fetcher: a subclass ofFetcherError,ExchangeError,
HttpStatusValidationErrororFetchTimeoutErrorpassesinstanceof
without callingObject.setPrototypeOfitself. The base classes reset the
prototype to their own, which broke subclasses that did not reset it again. -
@ahoo-wang/fetcher:fetchreceives a cleanRequestInit. The
fetcher-only fieldsurl,timeout,urlParamsandabortControllerwere
passed along in it; the URL is the first argument and cancellation is the
mergedsignal. -
Type declarations resolve under
moduleResolution: node16/nodenext: every
package ships.d.ctsfor itsrequirecondition next to.d.tsfor
import, and relative specifiers carry their extension (#1916). CommonJS
consumers no longer get TS1479/TS2834 errors, oranyunderskipLibCheck. -
@ahoo-wang/fetcher-cosec: the 401 refresh-retry in
AuthorizationResponseInterceptorreplays 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
onForbiddenfiring twice when the retry answers 403) and the rejectedExchangeErrornow
carries the retry's ownHttpStatusValidationErrorinexchange.error
rather than a nestedExchangeError; a successful retry no longer runs the response
interceptors ordered after it twice on the fresh response. -
@ahoo-wang/fetcher-eventstream: the globalResponseaugmentation declares
contentTypeandisEventStreamasreadonlyproperties (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.mtsand.ctsconsumers) failed withTS2300: Duplicate identifier
(#1920; also in 5.1.5).
Maintenance
- 5.x line. The
5.xbranch (cut fromv5.1.3) keeps receiving fixes.
After 6.0, 5.x patches are published under the npm dist-tagrelease-5, so
lateststays 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.ymlbuilds and packs the core
packages when a change touches one that Wow consumes (fetcher,
decorator,eventstream,openapi), links
them into a checkout of Wow throughpnpm.overrides, and runs Wow's
TypeScript build, type check and unit tests. It is advisory, not required. - CI runs a single
coresuite on Node 22 and 24; the view-engine shards, the
viewer suite andgenerator-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-serverwith a fixture of the live data (#1926). The cases that reach
external hosts (the OpenAI-compatible endpoint, the live JSONPlaceholder
site) run in the advisoryintegration-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-typespacks every published package,
checks it with@arethetypeswrong/cli, and type-checks an.mtsand a
.ctsconsumer of it in onetscrun undernode16andnodenext, which
catches declaration clashes the per-entry check cannot (#1916, #1920). - Agent skills. The
skills/plugin was rewritten for triggering and
accuracy, gainedfetcher-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 hasclaude plugin evalcases;pnpm eval:skillsruns 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.