Skip to content

Commit

Permalink
🐛 Fix abort/progress addons state isolation issue
Browse files Browse the repository at this point in the history
The addon state was shared between all requests and not properly isolated
  • Loading branch information
elbywan committed Jan 20, 2023
1 parent bd3575a commit 2b3a659
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 41 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 11 additions & 7 deletions src/addons/abort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,13 @@ export interface AbortResolver {
* ```
*/
const abort: () => WretchAddon<AbortWretch, AbortResolver> = () => {
let timeout = null
let fetchController = null
return {
beforeRequest(wretch, options) {
fetchController = wretch._config.polyfill("AbortController", false, true)
beforeRequest(wretch, options, state) {
const fetchController = wretch._config.polyfill("AbortController", false, true)
if (!options["signal"] && fetchController) {
options["signal"] = fetchController.signal
}
timeout = {
const timeout = {
ref: null,
clear() {
if (timeout.ref) {
Expand All @@ -125,19 +123,25 @@ const abort: () => WretchAddon<AbortWretch, AbortResolver> = () => {
}
}
}
state.abort = {
timeout,
fetchController
}
return wretch
},
wretch: {
signal(controller) {
return { ...this, _options: { ...this._options, signal: controller.signal } }
},
},
resolver: {
setTimeout(time, controller = fetchController) {
setTimeout(time, controller = this._sharedState.abort.fetchController) {
const { timeout } = this._sharedState.abort
timeout.clear()
timeout.ref = setTimeout(() => controller.abort(), time)
return this
},
controller() { return [fetchController, this] },
controller() { return [this._sharedState.abort.fetchController, this] },
onAbort(cb) { return this.error("AbortError", cb) }
},
}
Expand Down
58 changes: 28 additions & 30 deletions src/addons/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,43 +43,41 @@ export interface ProgressResolver {
* ```
*/
const progress: () => WretchAddon<unknown, ProgressResolver> = () => {
const cb = {
ref: null
}

const transformMiddleware: ConfiguredMiddleware = next => (url, opts) => {
let loaded = 0
let total = 0
return next(url, opts).then(response => {
try {
const contentLength = response.headers.get("content-length")
total = contentLength ? +contentLength : null
const transform = new TransformStream({
transform(chunk, controller) {
loaded += chunk.length
if (total < loaded) {
total = loaded
function transformMiddleware(state: Record<any, any>) : ConfiguredMiddleware {
return next => (url, opts) => {
let loaded = 0
let total = 0
return next(url, opts).then(response => {
try {
const contentLength = response.headers.get("content-length")
total = contentLength ? +contentLength : null
const transform = new TransformStream({
transform(chunk, controller) {
loaded += chunk.length
if (total < loaded) {
total = loaded
}
if (state.progress) {
state.progress(loaded, total)
}
controller.enqueue(chunk)
}
if (cb.ref) {
cb.ref(loaded, total)
}
controller.enqueue(chunk)
}
})
return new Response(response.body.pipeThrough(transform), response)
} catch (e) {
return response
}
})
})
return new Response(response.body.pipeThrough(transform), response)
} catch (e) {
return response
}
})
}
}

return {
beforeRequest(wretch) {
return wretch._middlewares.push(transformMiddleware)
beforeRequest(wretch, _, state) {
return wretch.middlewares([transformMiddleware(state)])
},
resolver: {
progress(onProgress: (loaded: number, total: number) => void) {
cb.ref = onProgress
this._sharedState.progress = onProgress
return this
}
},
Expand Down
11 changes: 10 additions & 1 deletion src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ export class WretchError extends Error implements WretchErrorType {
}

export const resolver = <T, Chain, R>(wretch: T & Wretch<T, Chain, R>) => {
const sharedState = Object.create(null)

wretch = wretch._addons.reduce((w, addon) =>
addon.beforeRequest &&
addon.beforeRequest(w, wretch._options, sharedState)
|| w,
wretch)

const {
_url: url,
_options: opts,
Expand All @@ -28,7 +36,7 @@ export const resolver = <T, Chain, R>(wretch: T & Wretch<T, Chain, R>) => {

const catchers = new Map(_catchers)
const finalOptions = mix(config.options, opts)
addons.forEach(addon => addon.beforeRequest && addon.beforeRequest(wretch, finalOptions))

// The generated fetch request
let finalUrl = url
const _fetchReq = middlewareHelper(middlewares)((url, options) => {
Expand Down Expand Up @@ -89,6 +97,7 @@ export const resolver = <T, Chain, R>(wretch: T & Wretch<T, Chain, R>) => {
const responseChain: WretchResponseChain<T, Chain, R> = {
_wretchReq: wretch,
_fetchReq,
_sharedState: sharedState,
res: bodyParser<WretchResponse>(null),
json: bodyParser<any>("json"),
blob: bodyParser<Blob>("blob"),
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,10 @@ export interface WretchResponseChain<T, Self = unknown, R = undefined> {
* @private @internal
*/
_fetchReq: Promise<WretchResponse>,
/**
* @private @internal
*/
_sharedState: Record<any, any>,

/**
*
Expand Down Expand Up @@ -758,7 +762,7 @@ export type FetchLike = (url: string, opts: WretchOptions) => Promise<WretchResp
* An addon enhancing either the request or response chain (or both).
*/
export type WretchAddon<W extends unknown, R extends unknown = unknown> = {
beforeRequest?<T, C, R>(wretch: T & Wretch<T, C, R>, options: WretchOptions): void,
beforeRequest?<T, C, R>(wretch: T & Wretch<T, C, R>, options: WretchOptions, state : Record<any, any>): T & Wretch<T, C, R>,
wretch?: W,
resolver?: R
}
Expand Down

0 comments on commit 2b3a659

Please sign in to comment.