Skip to content

Commit

Permalink
[Deprecation]: Remove internal objects from public API
Browse files Browse the repository at this point in the history
The `FetchRequest`, `FetchResponse`, and `FormSubmission` objects are
internal abstractions that facilitate Turbo's management of HTTP and
Form Submission life cycles.

Currently, references to instances of those classes are available
through `event.detail` for the following events:

* `turbo:frame-render`
* `turbo:before-fetch-request`
* `turbo:before-fetch-response`
* `turbo:submit-start`
* `turbo:submit-end`

Similarly, the `turbo:before-fetch-request` exposes a `fetchOptions`
object that is a separate instance from the one used to submit the
request. This means that **any** modifications to **any** value made
from within an event listener are ineffective and **do not change** the
ensuing request.

This commit deprecates those properties in favor of their built-in
foundations, namely:

* [Request][]
* [Response][]

The properties that expose those instances will remain as deprecations,
but will be inaccessible after an `8.0` release.

[Request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
  • Loading branch information
seanpdoyle committed Mar 29, 2024
1 parent 16a2d49 commit 1a1bdff
Show file tree
Hide file tree
Showing 20 changed files with 243 additions and 162 deletions.
55 changes: 49 additions & 6 deletions src/core/drive/form_submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class FormSubmission {
if (!fetchRequest.isSafe) {
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token")
if (token) {
fetchRequest.headers["X-CSRF-Token"] = token
fetchRequest.headers.set("X-CSRF-Token", token)
}
}

Expand All @@ -115,13 +115,24 @@ export class FormSubmission {
}

requestStarted(fetchRequest) {
const formSubmission = this

this.state = FormSubmissionState.waiting
this.submitter?.setAttribute("disabled", "")
this.setSubmitsWith()
markAsBusy(this.formElement)
dispatch("turbo:submit-start", {
target: this.formElement,
detail: { formSubmission: this }
detail: {
request: fetchRequest.request,
submitter: this.submitter,

get formSubmission() {
console.warn("`event.detail.formSubmission` is deprecated. Use `event.target`, `event.detail.submitter`, and `event.detail.request` instead")

return formSubmission
}
}
})
this.delegate.formSubmissionStarted(this)
}
Expand All @@ -145,13 +156,31 @@ export class FormSubmission {
this.delegate.formSubmissionErrored(this, error)
} else {
this.state = FormSubmissionState.receiving
this.result = { success: true, fetchResponse: fetchResponse }
this.result = {
success: true,
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
}
this.delegate.formSubmissionSucceededWithResponse(this, fetchResponse)
}
}

requestFailedWithResponse(fetchRequest, fetchResponse) {
this.result = { success: false, fetchResponse: fetchResponse }
this.result = {
success: false,
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
}
this.delegate.formSubmissionFailedWithResponse(this, fetchResponse)
}

Expand All @@ -160,14 +189,28 @@ export class FormSubmission {
this.delegate.formSubmissionErrored(this, error)
}

requestFinished(_fetchRequest) {
requestFinished(fetchRequest) {
const { formSubmission } = this

this.state = FormSubmissionState.stopped
this.submitter?.removeAttribute("disabled")
this.resetSubmitterText()
clearBusyState(this.formElement)

dispatch("turbo:submit-end", {
target: this.formElement,
detail: { formSubmission: this, ...this.result }
detail: {
request: fetchRequest.request,
submitter: this.submitter,

get formSubmission() {
console.warn("`event.detail.formSubmission` is deprecated. Use `event.target`, `event.detail.submitter`, and `event.detail.request` instead")

return formSubmission
},

...this.result
}
})
this.delegate.formSubmissionFinished(this)
}
Expand Down
12 changes: 6 additions & 6 deletions src/core/drive/prefetch_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ class PrefetchCache {

get(url) {
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
return this.#prefetched.request
return this.#prefetched.fetchRequest
}
}

setLater(url, request, ttl) {
setLater(url, fetchRequest, ttl) {
this.clear()

this.#prefetchTimeout = setTimeout(() => {
request.perform()
this.set(url, request, ttl)
fetchRequest.perform()
this.set(url, fetchRequest, ttl)
this.#prefetchTimeout = null
}, PREFETCH_DELAY)
}

set(url, request, ttl) {
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) }
set(url, fetchRequest, ttl) {
this.#prefetched = { url, fetchRequest, expire: new Date(new Date().getTime() + ttl) }
}

clear() {
Expand Down
2 changes: 1 addition & 1 deletion src/core/drive/preloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class Preloader {
// Fetch request delegate

prepareRequest(fetchRequest) {
fetchRequest.headers["X-Sec-Purpose"] = "prefetch"
fetchRequest.headers.set("X-Sec-Purpose", "prefetch")
}

async requestSucceededWithResponse(fetchRequest, fetchResponse) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class FrameController {
// Fetch request delegate

prepareRequest(request) {
request.headers["Turbo-Frame"] = this.id
request.headers.set("Turbo-Frame", this.id)

if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
request.acceptResponseType(StreamMessage.contentType)
Expand Down
4 changes: 2 additions & 2 deletions src/core/frames/link_interceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export class LinkInterceptor {

linkClicked = (event) => {
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.request.url, event.detail.originalEvent)) {
this.clickEvent.preventDefault()
event.preventDefault()
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent)
this.delegate.linkClickIntercepted(event.target, event.detail.request.url, event.detail.originalEvent)
}
}
delete this.clickEvent
Expand Down
10 changes: 9 additions & 1 deletion src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,15 @@ export class Session {

notifyApplicationAfterFrameRender(fetchResponse, frame) {
return dispatch("turbo:frame-render", {
detail: { fetchResponse },
detail: {
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
},
target: frame,
cancelable: true
})
Expand Down
13 changes: 8 additions & 5 deletions src/http/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ export const recentRequests = new LimitedSet(20)

const nativeFetch = window.fetch

function fetchWithTurboHeaders(url, options = {}) {
const modifiedHeaders = new Headers(options.headers || {})
function fetchWithTurboHeaders(resource, options = {}) {
const headers = resource instanceof Request ?
resource.headers :
new Headers(options.headers || {})

const requestUID = uuid()
recentRequests.add(requestUID)
modifiedHeaders.append("X-Turbo-Request-Id", requestUID)
headers.append("X-Turbo-Request-Id", requestUID)

return nativeFetch(url, {
return nativeFetch(resource, {
...options,
headers: modifiedHeaders
headers
})
}

Expand Down
91 changes: 53 additions & 38 deletions src/http/fetch_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,58 +48,38 @@ export class FetchRequest {
#resolveRequestPromise = (_value) => {}

constructor(delegate, method, location, requestBody = new URLSearchParams(), target = null, enctype = FetchEnctype.urlEncoded) {
method = fetchMethodFromString(method)

const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype)

this.delegate = delegate
this.url = url
this.target = target
this.fetchOptions = {
this.request = new Request(url.href, {
credentials: "same-origin",
redirect: "follow",
method: method,
headers: { ...this.defaultHeaders },
body: body,
signal: this.abortSignal,
referrer: this.delegate.referrer?.href
}
})
this.enctype = enctype
}

get method() {
return this.fetchOptions.method
}

set method(value) {
const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData()
const fetchMethod = fetchMethodFromString(value) || FetchMethod.get

this.url.search = ""

const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype)

this.url = url
this.fetchOptions.body = body
this.fetchOptions.method = fetchMethod
return this.request.method.toLowerCase()
}

get headers() {
return this.fetchOptions.headers
}

set headers(value) {
this.fetchOptions.headers = value
return this.request.headers
}

get body() {
if (this.isSafe) {
return this.url.searchParams
} else {
return this.fetchOptions.body
}
return this.request.body
}

set body(value) {
this.fetchOptions.body = value
get url() {
return expandURL(this.request.url)
}

get location() {
Expand All @@ -114,21 +94,35 @@ export class FetchRequest {
return this.body ? Array.from(this.body.entries()) : []
}

get fetchOptions() {
console.warn("`FetchRequest.fetchOptions` is deprecated. Read properties from `FetchRequest.request` instead")

return {
credentials: this.request.credentials,
redirect: this.request.redirect,
method: this.method,
headers: Object.fromEntries(this.request.headers.entries()),
body: this.body,
signal: this.request.signal,
referrer: this.request.referrer
}
}

cancel() {
this.abortController.abort()
}

async perform() {
const { fetchOptions } = this
this.delegate.prepareRequest(this)
const event = await this.#allowRequestToBeIntercepted(fetchOptions)
const event = await this.#allowRequestToBeIntercepted(this.request.url, fetchOptions)
try {
this.delegate.requestStarted(this)

if (event.detail.fetchRequest) {
this.response = event.detail.fetchRequest.response
} else {
this.response = fetch(this.url.href, fetchOptions)
this.response = fetch(this.request)
}

const response = await this.response
Expand All @@ -146,10 +140,20 @@ export class FetchRequest {
}

async receive(response) {
const { request } = this
const fetchResponse = new FetchResponse(response)
const event = dispatch("turbo:before-fetch-response", {
cancelable: true,
detail: { fetchResponse },
detail: {
request,
response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
},
target: this.target
})
if (event.defaultPrevented) {
Expand Down Expand Up @@ -177,21 +181,32 @@ export class FetchRequest {
}

acceptResponseType(mimeType) {
this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ")
this.headers.set("Accept", [mimeType, this.headers.get("Accept")].join(", "))
}

async #allowRequestToBeIntercepted(fetchOptions) {
async #allowRequestToBeIntercepted(url, fetchOptions) {
const requestInterception = new Promise((resolve) => (this.#resolveRequestPromise = resolve))
const event = dispatch("turbo:before-fetch-request", {
cancelable: true,
detail: {
fetchOptions,
url: this.url,
get fetchOptions() {
console.warn("`event.detail.fetchOptions` is deprecated. Use `event.detail.request` instead")

return fetchOptions
},

get url() {
console.warn("`event.detail.url` is deprecated. Use `event.detail.request.url` instead")

return url
},

request: this.request,
resume: this.#resolveRequestPromise
},
target: this.target
})
this.url = event.detail.url
this.request = event.detail.request
if (event.defaultPrevented) await requestInterception

return event
Expand All @@ -201,7 +216,7 @@ export class FetchRequest {
const event = dispatch("turbo:fetch-request-error", {
target: this.target,
cancelable: true,
detail: { request: this, error: error }
detail: { request: this.request, error: error }
})

return !event.defaultPrevented
Expand Down
Loading

0 comments on commit 1a1bdff

Please sign in to comment.